mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Ensure live status requests are always batched (#4713)
This commit is contained in:
parent
f915eab1a2
commit
76527073cf
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -76,6 +76,11 @@ public:
|
|||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ITwitchLiveController *getTwitchLiveController() override
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace chatterino::mock
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
190
src/controllers/twitch/LiveController.cpp
Normal file
190
src/controllers/twitch/LiveController.cpp
Normal 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
|
89
src/controllers/twitch/LiveController.hpp
Normal file
89
src/controllers/twitch/LiveController.hpp
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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('#'))
|
||||
|
|
|
@ -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_;
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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_});
|
||||
|
|
Loading…
Reference in a new issue