Ensure live status requests are always batched (#4713)

This commit is contained in:
pajlada 2023-07-02 15:52:15 +02:00 committed by GitHub
parent f915eab1a2
commit 76527073cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 582 additions and 282 deletions

View file

@ -49,6 +49,8 @@ CheckOptions:
value: CamelCase
- key: readability-identifier-naming.GlobalConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.GlobalVariableCase
value: UPPER_CASE
- key: readability-identifier-naming.VariableCase
value: camelBack
- key: readability-implicit-bool-conversion.AllowPointerConditions

View file

@ -11,6 +11,7 @@
- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680)
- Minor: Added a message for when Chatterino joins a channel (#4616)
- Minor: Add pin action to usercards and reply threads. (#4692)
- Minor: Stream status requests are now batched. (#4713)
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
- Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675)

View file

@ -76,6 +76,11 @@ public:
{
return nullptr;
}
ITwitchLiveController *getTwitchLiveController() override
{
return nullptr;
}
};
} // namespace chatterino::mock

View file

@ -86,6 +86,12 @@ public:
std::function<void()> finallyCallback),
(override));
MOCK_METHOD(void, fetchChannels,
(QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getChannel,
(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,

View file

@ -14,6 +14,7 @@
# include "controllers/plugins/PluginController.hpp"
#endif
#include "controllers/sound/SoundController.hpp"
#include "controllers/twitch/LiveController.hpp"
#include "controllers/userdata/UserDataController.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "messages/Message.hpp"
@ -88,6 +89,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, seventvBadges(&this->emplace<SeventvBadges>())
, userData(&this->emplace<UserDataController>())
, sound(&this->emplace<SoundController>())
, twitchLiveController(&this->emplace<TwitchLiveController>())
#ifdef CHATTERINO_HAVE_PLUGINS
, plugins(&this->emplace<PluginController>())
#endif
@ -245,6 +247,11 @@ IUserDataController *Application::getUserData()
return this->userData;
}
ITwitchLiveController *Application::getTwitchLiveController()
{
return this->twitchLiveController;
}
ITwitchIrcServer *Application::getTwitch()
{
return this->twitch;

View file

@ -23,6 +23,8 @@ class HotkeyController;
class IUserDataController;
class UserDataController;
class SoundController;
class ITwitchLiveController;
class TwitchLiveController;
#ifdef CHATTERINO_HAVE_PLUGINS
class PluginController;
#endif
@ -62,6 +64,7 @@ public:
virtual ChatterinoBadges *getChatterinoBadges() = 0;
virtual FfzBadges *getFfzBadges() = 0;
virtual IUserDataController *getUserData() = 0;
virtual ITwitchLiveController *getTwitchLiveController() = 0;
};
class Application : public IApplication
@ -101,6 +104,10 @@ public:
UserDataController *const userData{};
SoundController *const sound{};
private:
TwitchLiveController *const twitchLiveController{};
public:
#ifdef CHATTERINO_HAVE_PLUGINS
PluginController *const plugins{};
#endif
@ -154,6 +161,7 @@ public:
return this->ffzBadges;
}
IUserDataController *getUserData() override;
ITwitchLiveController *getTwitchLiveController() override;
pajlada::Signals::NoArgSignal streamerModeChanged;

View file

@ -167,13 +167,16 @@ set(SOURCE_FILES
controllers/plugins/LuaUtilities.cpp
controllers/plugins/LuaUtilities.hpp
controllers/sound/SoundController.cpp
controllers/sound/SoundController.hpp
controllers/twitch/LiveController.cpp
controllers/twitch/LiveController.hpp
controllers/userdata/UserDataController.cpp
controllers/userdata/UserDataController.hpp
controllers/userdata/UserData.hpp
controllers/sound/SoundController.cpp
controllers/sound/SoundController.hpp
debug/Benchmark.cpp
debug/Benchmark.hpp

View file

@ -48,6 +48,8 @@ Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTwitchLiveController,
"chatterino.twitch.livecontroller", logThreshold);
Q_LOGGING_CATEGORY(chatterinoUpdate, "chatterino.update", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", logThreshold);

View file

@ -37,6 +37,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTheme);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitchLiveController);
Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);

View file

@ -0,0 +1,190 @@
#include "controllers/twitch/LiveController.hpp"
#include "common/QLogging.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "util/Helpers.hpp"
#include <QDebug>
namespace {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
const auto &LOG = chatterinoTwitchLiveController;
} // namespace
namespace chatterino {
TwitchLiveController::TwitchLiveController()
{
QObject::connect(&this->refreshTimer, &QTimer::timeout, [this] {
this->request();
});
this->refreshTimer.start(TwitchLiveController::REFRESH_INTERVAL);
QObject::connect(&this->immediateRequestTimer, &QTimer::timeout, [this] {
QStringList channelIDs;
{
std::unique_lock immediateRequestsLock(
this->immediateRequestsMutex);
for (const auto &channelID : this->immediateRequests)
{
channelIDs.append(channelID);
}
this->immediateRequests.clear();
}
if (channelIDs.isEmpty())
{
return;
}
this->request(channelIDs);
});
this->immediateRequestTimer.start(
TwitchLiveController::IMMEDIATE_REQUEST_INTERVAL);
}
void TwitchLiveController::add(const std::shared_ptr<TwitchChannel> &newChannel)
{
assert(newChannel != nullptr);
const auto channelID = newChannel->roomId();
assert(!channelID.isEmpty());
{
std::unique_lock lock(this->channelsMutex);
this->channels[channelID] = newChannel;
}
{
std::unique_lock immediateRequestsLock(this->immediateRequestsMutex);
this->immediateRequests.emplace(channelID);
}
}
void TwitchLiveController::request(std::optional<QStringList> optChannelIDs)
{
QStringList channelIDs;
if (optChannelIDs)
{
channelIDs = *optChannelIDs;
}
else
{
std::shared_lock lock(this->channelsMutex);
for (const auto &channelList : this->channels)
{
channelIDs.append(channelList.first);
}
}
if (channelIDs.isEmpty())
{
return;
}
auto batches =
splitListIntoBatches(channelIDs, TwitchLiveController::BATCH_SIZE);
qCDebug(LOG) << "Make" << batches.size() << "requests";
for (const auto &batch : batches)
{
// TODO: Explore making this concurrent
getHelix()->fetchStreams(
batch, {},
[this, batch{batch}](const auto &streams) {
std::unordered_map<QString, std::optional<HelixStream>> results;
for (const auto &channelID : batch)
{
results[channelID] = std::nullopt;
}
for (const auto &stream : streams)
{
results[stream.userId] = stream;
}
QStringList deadChannels;
{
std::shared_lock lock(this->channelsMutex);
for (const auto &result : results)
{
auto it = this->channels.find(result.first);
if (it != channels.end())
{
if (auto channel = it->second.lock(); channel)
{
channel->updateStreamStatus(result.second);
}
else
{
deadChannels.append(result.first);
}
}
}
}
if (!deadChannels.isEmpty())
{
std::unique_lock lock(this->channelsMutex);
for (const auto &deadChannel : deadChannels)
{
this->channels.erase(deadChannel);
}
}
},
[] {
qCWarning(LOG) << "Failed stream check request";
},
[] {});
// TODO: Explore making this concurrent
getHelix()->fetchChannels(
batch,
[this, batch{batch}](const auto &helixChannels) {
QStringList deadChannels;
{
std::shared_lock lock(this->channelsMutex);
for (const auto &helixChannel : helixChannels)
{
auto it = this->channels.find(helixChannel.userId);
if (it != this->channels.end())
{
if (auto channel = it->second.lock(); channel)
{
channel->updateStreamTitle(helixChannel.title);
channel->updateDisplayName(helixChannel.name);
}
else
{
deadChannels.append(helixChannel.userId);
}
}
}
}
if (!deadChannels.isEmpty())
{
std::unique_lock lock(this->channelsMutex);
for (const auto &deadChannel : deadChannels)
{
this->channels.erase(deadChannel);
}
}
},
[] {
qCWarning(LOG) << "Failed stream check request";
});
}
}
} // namespace chatterino

View file

@ -0,0 +1,89 @@
#pragma once
#include "common/Singleton.hpp"
#include "util/QStringHash.hpp"
#include <QString>
#include <QTimer>
#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <unordered_set>
namespace chatterino {
class TwitchChannel;
class ITwitchLiveController
{
public:
virtual ~ITwitchLiveController() = default;
virtual void add(const std::shared_ptr<TwitchChannel> &newChannel) = 0;
};
class TwitchLiveController : public ITwitchLiveController, public Singleton
{
public:
// Controls how often all channels have their stream status refreshed
static constexpr std::chrono::seconds REFRESH_INTERVAL{30};
// Controls how quickly new channels have their stream status loaded
static constexpr std::chrono::seconds IMMEDIATE_REQUEST_INTERVAL{1};
/**
* How many channels to include in a single request
*
* Should not be more than 100
**/
static constexpr int BATCH_SIZE{100};
TwitchLiveController();
// Add a Twitch channel to be queried for live status
// A request is made within a few seconds if this is the first time this channel is added
void add(const std::shared_ptr<TwitchChannel> &newChannel) override;
private:
/**
* Run batched Helix Channels & Stream requests for channels
*
* If a list of channel IDs is passed to request, we only make a request for those channels
*
* If no list of channels is passed to request (the default behaviour), we make requests for all channels
* in the `channels` map.
**/
void request(std::optional<QStringList> optChannelIDs = std::nullopt);
/**
* List of channel IDs pointing to their Twitch Channel
*
* These channels will have their stream status updated every REFRESH_INTERVAL seconds
**/
std::unordered_map<QString, std::weak_ptr<TwitchChannel>> channels;
std::shared_mutex channelsMutex;
/**
* List of channels that need an immediate live status update
*
* These channels will have their stream status updated after at most IMMEDIATE_REQUEST_INTERVAL seconds
**/
std::unordered_set<QString> immediateRequests;
std::mutex immediateRequestsMutex;
/**
* Timer responsible for refreshing `channels`
**/
QTimer refreshTimer;
/**
* Timer responsible for refreshing `immediateRequests`
**/
QTimer immediateRequestTimer;
};
} // namespace chatterino

View file

@ -8,6 +8,7 @@
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/twitch/LiveController.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/Link.hpp"
@ -98,14 +99,14 @@ TwitchChannel::TwitchChannel(const QString &name)
// room id loaded -> refresh live status
this->roomIdChanged.connect([this]() {
this->refreshPubSub();
this->refreshTitle();
this->refreshLiveStatus();
this->refreshBadges();
this->refreshCheerEmotes();
this->refreshFFZChannelEmotes(false);
this->refreshBTTVChannelEmotes(false);
this->refreshSevenTVChannelEmotes(false);
this->joinBttvChannel();
getIApp()->getTwitchLiveController()->add(
std::dynamic_pointer_cast<TwitchChannel>(shared_from_this()));
});
this->connected.connect([this]() {
@ -151,6 +152,85 @@ TwitchChannel::TwitchChannel(const QString &name)
});
this->threadClearTimer_.start(5 * 60 * 1000);
auto onLiveStatusChanged = [this](auto isLive) {
if (isLive)
{
qCDebug(chatterinoTwitch)
<< "[TwitchChannel" << this->getName() << "] Online";
if (getApp()->notifications->isChannelNotified(this->getName(),
Platform::Twitch))
{
if (Toasts::isEnabled())
{
getApp()->toasts->sendChannelNotification(
this->getName(), this->accessStreamStatus()->title,
Platform::Twitch);
}
if (getSettings()->notificationPlaySound)
{
getApp()->notifications->playSound();
}
if (getSettings()->notificationFlashTaskbar)
{
getApp()->windows->sendAlert();
}
}
// Channel live message
MessageBuilder builder;
TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// Message in /live channel
MessageBuilder builder2;
TwitchMessageBuilder::liveMessage(this->getDisplayName(),
&builder2);
getApp()->twitch->liveChannel->addMessage(builder2.release());
// Notify on all channels with a ping sound
if (getSettings()->notificationOnAnyChannel &&
!(isInStreamerMode() &&
getSettings()->streamerModeSuppressLiveNotifications))
{
getApp()->notifications->playSound();
}
}
else
{
qCDebug(chatterinoTwitch)
<< "[TwitchChannel" << this->getName() << "] Offline";
// Channel offline message
MessageBuilder builder;
TwitchMessageBuilder::offlineSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// "delete" old 'CHANNEL is live' message
LimitedQueueSnapshot<MessagePtr> snapshot =
getApp()->twitch->liveChannel->getMessageSnapshot();
int snapshotLength = snapshot.size();
// MSVC hates this code if the parens are not there
int end = (std::max)(0, snapshotLength - 200);
auto liveMessageSearchText =
QString("%1 is live!").arg(this->getDisplayName());
for (int i = snapshotLength - 1; i >= end; --i)
{
const auto &s = snapshot[i];
if (s->messageText == liveMessageSearchText)
{
s->flags.set(MessageFlag::Disabled);
break;
}
}
}
};
this->signalHolder_.managedConnect(this->liveStatusChanged,
onLiveStatusChanged);
// debugging
#if 0
for (int i = 0; i < 1000; i++) {
@ -172,7 +252,6 @@ TwitchChannel::~TwitchChannel()
void TwitchChannel::initialize()
{
this->fetchDisplayName();
this->refreshChatters();
this->refreshBadges();
}
@ -340,6 +419,89 @@ boost::optional<ChannelPointReward> TwitchChannel::channelPointReward(
return it->second;
}
void TwitchChannel::updateStreamStatus(
const std::optional<HelixStream> &helixStream)
{
if (helixStream)
{
auto stream = *helixStream;
{
auto status = this->streamStatus_.access();
status->viewerCount = stream.viewerCount;
status->gameId = stream.gameId;
status->game = stream.gameName;
status->title = stream.title;
QDateTime since =
QDateTime::fromString(stream.startedAt, Qt::ISODate);
auto diff = since.secsTo(QDateTime::currentDateTime());
status->uptime = QString::number(diff / 3600) + "h " +
QString::number(diff % 3600 / 60) + "m";
status->rerun = false;
status->streamType = stream.type;
}
if (this->setLive(true))
{
this->liveStatusChanged.invoke(true);
}
this->streamStatusChanged.invoke();
}
else
{
if (this->setLive(false))
{
this->liveStatusChanged.invoke(false);
this->streamStatusChanged.invoke();
}
}
}
void TwitchChannel::updateStreamTitle(const QString &title)
{
{
auto status = this->streamStatus_.access();
if (status->title == title)
{
// Title has not changed
return;
}
status->title = title;
}
this->streamStatusChanged.invoke();
}
void TwitchChannel::updateDisplayName(const QString &displayName)
{
if (displayName == this->getDisplayName())
{
// Display name has not changed
return;
}
// Display name has changed
if (QString::compare(displayName, this->getName(), Qt::CaseInsensitive) ==
0)
{
// Display name is only a case variation of the login name
this->setDisplayName(displayName);
this->setLocalizedName(displayName);
}
else
{
// Display name contains Chinese, Japanese, or Korean characters
this->setDisplayName(this->getName());
this->setLocalizedName(
QString("%1(%2)").arg(this->getName()).arg(displayName));
}
this->addRecentChatter(this->getDisplayName());
this->displayNameChanged.invoke();
}
void TwitchChannel::showLoginMessage()
{
const auto linkColor = MessageColor(MessageColor::Link);
@ -909,183 +1071,16 @@ int TwitchChannel::chatterCount()
return this->chatterCount_;
}
void TwitchChannel::setLive(bool newLiveStatus)
bool TwitchChannel::setLive(bool newLiveStatus)
{
auto guard = this->streamStatus_.access();
if (guard->live == newLiveStatus)
{
auto guard = this->streamStatus_.access();
if (guard->live == newLiveStatus)
{
return;
}
guard->live = newLiveStatus;
return false;
}
guard->live = newLiveStatus;
if (newLiveStatus)
{
if (getApp()->notifications->isChannelNotified(this->getName(),
Platform::Twitch))
{
if (Toasts::isEnabled())
{
getApp()->toasts->sendChannelNotification(
this->getName(), this->accessStreamStatus()->title,
Platform::Twitch);
}
if (getSettings()->notificationPlaySound)
{
getApp()->notifications->playSound();
}
if (getSettings()->notificationFlashTaskbar)
{
getApp()->windows->sendAlert();
}
}
// Channel live message
MessageBuilder builder;
TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// Message in /live channel
MessageBuilder builder2;
TwitchMessageBuilder::liveMessage(this->getDisplayName(), &builder2);
getApp()->twitch->liveChannel->addMessage(builder2.release());
// Notify on all channels with a ping sound
if (getSettings()->notificationOnAnyChannel &&
!(isInStreamerMode() &&
getSettings()->streamerModeSuppressLiveNotifications))
{
getApp()->notifications->playSound();
}
}
else
{
// Channel offline message
MessageBuilder builder;
TwitchMessageBuilder::offlineSystemMessage(this->getDisplayName(),
&builder);
this->addMessage(builder.release());
// "delete" old 'CHANNEL is live' message
LimitedQueueSnapshot<MessagePtr> snapshot =
getApp()->twitch->liveChannel->getMessageSnapshot();
int snapshotLength = snapshot.size();
// MSVC hates this code if the parens are not there
int end = (std::max)(0, snapshotLength - 200);
auto liveMessageSearchText =
QString("%1 is live!").arg(this->getDisplayName());
for (int i = snapshotLength - 1; i >= end; --i)
{
auto &s = snapshot[i];
if (s->messageText == liveMessageSearchText)
{
s->flags.set(MessageFlag::Disabled);
break;
}
}
}
this->liveStatusChanged.invoke();
}
void TwitchChannel::refreshTitle()
{
// timer has never started, proceed and start it
if (!this->titleRefreshedTimer_.isValid())
{
this->titleRefreshedTimer_.start();
}
else if (this->roomId().isEmpty() ||
this->titleRefreshedTimer_.elapsed() < TITLE_REFRESH_PERIOD)
{
return;
}
this->titleRefreshedTimer_.restart();
getHelix()->getChannel(
this->roomId(),
[this, weak = weakOf<Channel>(this)](HelixChannel channel) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
{
auto status = this->streamStatus_.access();
status->title = channel.title;
}
this->liveStatusChanged.invoke();
},
[] {
// failure
});
}
void TwitchChannel::refreshLiveStatus()
{
auto roomID = this->roomId();
if (roomID.isEmpty())
{
qCDebug(chatterinoTwitch) << "[TwitchChannel" << this->getName()
<< "] Refreshing live status (Missing ID)";
this->setLive(false);
return;
}
getHelix()->getStreamById(
roomID,
[this, weak = weakOf<Channel>(this)](bool live, const auto &stream) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
this->parseLiveStatus(live, stream);
},
[] {
// failure
},
[] {
// finally
});
}
void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
{
if (!live)
{
this->setLive(false);
return;
}
{
auto status = this->streamStatus_.access();
status->viewerCount = stream.viewerCount;
status->gameId = stream.gameId;
status->game = stream.gameName;
status->title = stream.title;
QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate);
auto diff = since.secsTo(QDateTime::currentDateTime());
status->uptime = QString::number(diff / 3600) + "h " +
QString::number(diff % 3600 / 60) + "m";
status->rerun = false;
status->streamType = stream.type;
}
this->setLive(true);
// Signal all listeners that the stream status has been updated
this->liveStatusChanged.invoke();
return true;
}
void TwitchChannel::loadRecentMessages()
@ -1220,33 +1215,6 @@ void TwitchChannel::refreshChatters()
[](auto error, auto message) {});
}
void TwitchChannel::fetchDisplayName()
{
getHelix()->getUserByName(
this->getName(),
[weak = weakOf<Channel>(this)](const auto &user) {
auto shared = weak.lock();
if (!shared)
return;
auto channel = static_cast<TwitchChannel *>(shared.get());
if (QString::compare(user.displayName, channel->getName(),
Qt::CaseInsensitive) == 0)
{
channel->setDisplayName(user.displayName);
channel->setLocalizedName(user.displayName);
}
else
{
channel->setLocalizedName(QString("%1(%2)")
.arg(channel->getName())
.arg(user.displayName));
}
channel->addRecentChatter(channel->getDisplayName());
channel->displayNameChanged.invoke();
},
[] {});
}
void TwitchChannel::addReplyThread(const std::shared_ptr<MessageThread> &thread)
{
this->threads_[thread->rootId()] = thread;

View file

@ -18,6 +18,7 @@
#include <atomic>
#include <mutex>
#include <optional>
#include <unordered_map>
namespace chatterino {
@ -120,7 +121,6 @@ public:
virtual bool hasHighRateLimit() const override;
virtual bool canReconnect() const override;
virtual void reconnect() override;
void refreshTitle();
void createClip();
// Data
@ -194,7 +194,23 @@ public:
// Signals
pajlada::Signals::NoArgSignal roomIdChanged;
pajlada::Signals::NoArgSignal userStateChanged;
pajlada::Signals::NoArgSignal liveStatusChanged;
/**
* This signals fires whenever the live status is changed
*
* Streams are counted as offline by default, so if a stream does not go online
* this signal will never fire
**/
pajlada::Signals::Signal<bool> liveStatusChanged;
/**
* This signal fires whenever the stream status is changed
*
* This includes when the stream goes from offline to online,
* or the viewer count changes, or the title has been updated
**/
pajlada::Signals::NoArgSignal streamStatusChanged;
pajlada::Signals::NoArgSignal roomModesChanged;
// Channel point rewards
@ -205,6 +221,12 @@ public:
boost::optional<ChannelPointReward> channelPointReward(
const QString &rewardId) const;
// Live status
void updateStreamStatus(const std::optional<HelixStream> &helixStream);
void updateStreamTitle(const QString &title);
void updateDisplayName(const QString &displayName);
private:
struct NameOptions {
QString displayName;
@ -213,21 +235,23 @@ private:
private:
// Methods
void refreshLiveStatus();
void parseLiveStatus(bool live, const HelixStream &stream);
void refreshPubSub();
void refreshChatters();
void refreshBadges();
void refreshCheerEmotes();
void loadRecentMessages();
void loadRecentMessagesReconnect();
void fetchDisplayName();
void cleanUpReplyThreads();
void showLoginMessage();
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
void joinBttvChannel() const;
void setLive(bool newLiveStatus);
/**
* @brief Sets the live status of this Twitch channel
*
* Returns true if the live status changed with this call
**/
bool setLive(bool newLiveStatus);
void setMod(bool value);
void setVIP(bool value);
void setStaff(bool value);
@ -236,7 +260,16 @@ private:
void setDisplayName(const QString &name);
void setLocalizedName(const QString &name);
/**
* Returns the display name of the user
*
* If the display name contained chinese, japenese, or korean characters, the user's login name is returned instead
**/
const QString &getDisplayName() const override;
/**
* Returns the localized name of the user
**/
const QString &getLocalizedName() const override;
QString prepareMessage(const QString &message) const;

View file

@ -80,12 +80,6 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
this->reloadBTTVGlobalEmotes();
this->reloadFFZGlobalEmotes();
this->reloadSevenTVGlobalEmotes();
/* Refresh all twitch channel's live status in bulk every 30 seconds after starting chatterino */
QObject::connect(&this->bulkLiveStatusTimer_, &QTimer::timeout, [this] {
this->bulkRefreshLiveStatus();
});
this->bulkLiveStatusTimer_.start(30 * 1000);
}
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
@ -412,59 +406,6 @@ std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
return Channel::getEmpty();
}
void TwitchIrcServer::bulkRefreshLiveStatus()
{
auto twitchChans = std::make_shared<QHash<QString, TwitchChannel *>>();
this->forEachChannel([twitchChans](ChannelPtr chan) {
auto tc = dynamic_cast<TwitchChannel *>(chan.get());
if (tc && !tc->roomId().isEmpty())
{
twitchChans->insert(tc->roomId(), tc);
}
});
// iterate over batches of channel IDs
for (const auto &batch : splitListIntoBatches(twitchChans->keys()))
{
getHelix()->fetchStreams(
batch, {},
[twitchChans](std::vector<HelixStream> streams) {
for (const auto &stream : streams)
{
// remaining channels will be used later to set their stream status as offline
// so we use take(id) to remove it
auto tc = twitchChans->take(stream.userId);
if (tc == nullptr)
{
continue;
}
tc->parseLiveStatus(true, stream);
}
},
[]() {
// failure
},
[batch, twitchChans] {
// All the channels that were not present in fetchStreams response should be assumed to be offline
// It is necessary to update their stream status in case they've gone live -> offline
// Otherwise some of them will be marked as live forever
for (const auto &chID : batch)
{
auto tc = twitchChans->value(chID);
// early out in case channel does not exist anymore
if (tc == nullptr)
{
continue;
}
tc->parseLiveStatus(false, {});
}
});
}
}
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
{
if (dirtyChannelName.startsWith('#'))

View file

@ -49,8 +49,6 @@ public:
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
void bulkRefreshLiveStatus();
void reloadBTTVGlobalEmotes();
void reloadAllBTTVChannelEmotes();
void reloadFFZGlobalEmotes();
@ -124,7 +122,6 @@ private:
BttvEmotes bttv;
FfzEmotes ffz;
SeventvEmotes seventv_;
QTimer bulkLiveStatusTimer_;
pajlada::Signals::SignalHolder signalHolder_;
};

View file

@ -417,6 +417,45 @@ void Helix::createClip(QString channelId,
.execute();
}
void Helix::fetchChannels(
QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
for (const auto &userID : userIDs)
{
urlQuery.addQueryItem("broadcaster_id", userID);
}
this->makeGet("channels", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixChannel> channels;
for (const auto &unparsedChannel : data.toArray())
{
channels.emplace_back(unparsedChannel.toObject());
}
successCallback(channels);
return Success;
})
.onError([failureCallback](auto /*result*/) {
failureCallback();
})
.execute();
}
void Helix::getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
HelixFailureCallback failureCallback)

View file

@ -788,6 +788,12 @@ public:
std::function<void(HelixClipError)> failureCallback,
std::function<void()> finallyCallback) = 0;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
virtual void fetchChannels(
QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback) = 0;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
virtual void getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
@ -1101,6 +1107,12 @@ public:
std::function<void(HelixClipError)> failureCallback,
std::function<void()> finallyCallback) final;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
void fetchChannels(
QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback) final;
// https://dev.twitch.tv/docs/api/reference#get-channel-information
void getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,

View file

@ -43,7 +43,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams
Used in:
- `TwitchChannel` to get live status, game, title, and viewer count of a channel
- `LiveController` to get live status, game, title, and viewer count of a channel
- `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
### Create Clip
@ -61,7 +61,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information
Used in:
- `TwitchChannel` to refresh stream title
- `LiveController` to refresh stream title & display name
### Update Channel

View file

@ -800,7 +800,7 @@ void ChannelView::setChannel(ChannelPtr underlyingChannel)
if (auto tc = dynamic_cast<TwitchChannel *>(underlyingChannel.get()))
{
this->channelConnections_.managedConnect(
tc->liveStatusChanged, [this]() {
tc->streamStatusChanged, [this]() {
this->liveStatusChanged.invoke();
});
}

View file

@ -740,7 +740,7 @@ void SplitHeader::handleChannelChanged()
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
{
this->channelConnections_.managedConnect(
twitchChannel->liveStatusChanged, [this]() {
twitchChannel->streamStatusChanged, [this]() {
this->updateChannelText();
});
}
@ -956,10 +956,6 @@ void SplitHeader::enterEvent(QEvent *event)
if (!this->tooltipText_.isEmpty())
{
auto *channel = this->split_->getChannel().get();
if (channel->getType() == Channel::Type::Twitch)
{
dynamic_cast<TwitchChannel *>(channel)->refreshTitle();
}
auto *tooltip = TooltipWidget::instance();
tooltip->setOne({nullptr, this->tooltipText_});