Ensure live status requests are always batched (#4713)
This commit is contained in:
@@ -49,6 +49,8 @@ CheckOptions:
|
|||||||
value: CamelCase
|
value: CamelCase
|
||||||
- key: readability-identifier-naming.GlobalConstantCase
|
- key: readability-identifier-naming.GlobalConstantCase
|
||||||
value: UPPER_CASE
|
value: UPPER_CASE
|
||||||
|
- key: readability-identifier-naming.GlobalVariableCase
|
||||||
|
value: UPPER_CASE
|
||||||
- key: readability-identifier-naming.VariableCase
|
- key: readability-identifier-naming.VariableCase
|
||||||
value: camelBack
|
value: camelBack
|
||||||
- key: readability-implicit-bool-conversion.AllowPointerConditions
|
- key: readability-implicit-bool-conversion.AllowPointerConditions
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680)
|
- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680)
|
||||||
- Minor: Added a message for when Chatterino joins a channel (#4616)
|
- Minor: Added a message for when Chatterino joins a channel (#4616)
|
||||||
- Minor: Add pin action to usercards and reply threads. (#4692)
|
- 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: 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: 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)
|
- Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675)
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ public:
|
|||||||
{
|
{
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ITwitchLiveController *getTwitchLiveController() override
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino::mock
|
} // namespace chatterino::mock
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ public:
|
|||||||
std::function<void()> finallyCallback),
|
std::function<void()> finallyCallback),
|
||||||
(override));
|
(override));
|
||||||
|
|
||||||
|
MOCK_METHOD(void, fetchChannels,
|
||||||
|
(QStringList userIDs,
|
||||||
|
ResultCallback<std::vector<HelixChannel>> successCallback,
|
||||||
|
HelixFailureCallback failureCallback),
|
||||||
|
(override));
|
||||||
|
|
||||||
MOCK_METHOD(void, getChannel,
|
MOCK_METHOD(void, getChannel,
|
||||||
(QString broadcasterId,
|
(QString broadcasterId,
|
||||||
ResultCallback<HelixChannel> successCallback,
|
ResultCallback<HelixChannel> successCallback,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
#endif
|
#endif
|
||||||
#include "controllers/sound/SoundController.hpp"
|
#include "controllers/sound/SoundController.hpp"
|
||||||
|
#include "controllers/twitch/LiveController.hpp"
|
||||||
#include "controllers/userdata/UserDataController.hpp"
|
#include "controllers/userdata/UserDataController.hpp"
|
||||||
#include "debug/AssertInGuiThread.hpp"
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
#include "messages/Message.hpp"
|
#include "messages/Message.hpp"
|
||||||
@@ -88,6 +89,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||||||
, seventvBadges(&this->emplace<SeventvBadges>())
|
, seventvBadges(&this->emplace<SeventvBadges>())
|
||||||
, userData(&this->emplace<UserDataController>())
|
, userData(&this->emplace<UserDataController>())
|
||||||
, sound(&this->emplace<SoundController>())
|
, sound(&this->emplace<SoundController>())
|
||||||
|
, twitchLiveController(&this->emplace<TwitchLiveController>())
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
, plugins(&this->emplace<PluginController>())
|
, plugins(&this->emplace<PluginController>())
|
||||||
#endif
|
#endif
|
||||||
@@ -245,6 +247,11 @@ IUserDataController *Application::getUserData()
|
|||||||
return this->userData;
|
return this->userData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ITwitchLiveController *Application::getTwitchLiveController()
|
||||||
|
{
|
||||||
|
return this->twitchLiveController;
|
||||||
|
}
|
||||||
|
|
||||||
ITwitchIrcServer *Application::getTwitch()
|
ITwitchIrcServer *Application::getTwitch()
|
||||||
{
|
{
|
||||||
return this->twitch;
|
return this->twitch;
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class HotkeyController;
|
|||||||
class IUserDataController;
|
class IUserDataController;
|
||||||
class UserDataController;
|
class UserDataController;
|
||||||
class SoundController;
|
class SoundController;
|
||||||
|
class ITwitchLiveController;
|
||||||
|
class TwitchLiveController;
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
class PluginController;
|
class PluginController;
|
||||||
#endif
|
#endif
|
||||||
@@ -62,6 +64,7 @@ public:
|
|||||||
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
||||||
virtual FfzBadges *getFfzBadges() = 0;
|
virtual FfzBadges *getFfzBadges() = 0;
|
||||||
virtual IUserDataController *getUserData() = 0;
|
virtual IUserDataController *getUserData() = 0;
|
||||||
|
virtual ITwitchLiveController *getTwitchLiveController() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Application : public IApplication
|
class Application : public IApplication
|
||||||
@@ -101,6 +104,10 @@ public:
|
|||||||
UserDataController *const userData{};
|
UserDataController *const userData{};
|
||||||
SoundController *const sound{};
|
SoundController *const sound{};
|
||||||
|
|
||||||
|
private:
|
||||||
|
TwitchLiveController *const twitchLiveController{};
|
||||||
|
|
||||||
|
public:
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
PluginController *const plugins{};
|
PluginController *const plugins{};
|
||||||
#endif
|
#endif
|
||||||
@@ -154,6 +161,7 @@ public:
|
|||||||
return this->ffzBadges;
|
return this->ffzBadges;
|
||||||
}
|
}
|
||||||
IUserDataController *getUserData() override;
|
IUserDataController *getUserData() override;
|
||||||
|
ITwitchLiveController *getTwitchLiveController() override;
|
||||||
|
|
||||||
pajlada::Signals::NoArgSignal streamerModeChanged;
|
pajlada::Signals::NoArgSignal streamerModeChanged;
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -167,13 +167,16 @@ set(SOURCE_FILES
|
|||||||
controllers/plugins/LuaUtilities.cpp
|
controllers/plugins/LuaUtilities.cpp
|
||||||
controllers/plugins/LuaUtilities.hpp
|
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.cpp
|
||||||
controllers/userdata/UserDataController.hpp
|
controllers/userdata/UserDataController.hpp
|
||||||
controllers/userdata/UserData.hpp
|
controllers/userdata/UserData.hpp
|
||||||
|
|
||||||
controllers/sound/SoundController.cpp
|
|
||||||
controllers/sound/SoundController.hpp
|
|
||||||
|
|
||||||
debug/Benchmark.cpp
|
debug/Benchmark.cpp
|
||||||
debug/Benchmark.hpp
|
debug/Benchmark.hpp
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
|
|||||||
Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", 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(chatterinoUpdate, "chatterino.update", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold);
|
Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold);
|
||||||
Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", 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(chatterinoTheme);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch);
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitchLiveController);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
|
||||||
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);
|
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
#include "controllers/notifications/NotificationController.hpp"
|
#include "controllers/notifications/NotificationController.hpp"
|
||||||
|
#include "controllers/twitch/LiveController.hpp"
|
||||||
#include "messages/Emote.hpp"
|
#include "messages/Emote.hpp"
|
||||||
#include "messages/Image.hpp"
|
#include "messages/Image.hpp"
|
||||||
#include "messages/Link.hpp"
|
#include "messages/Link.hpp"
|
||||||
@@ -98,14 +99,14 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||||||
// room id loaded -> refresh live status
|
// room id loaded -> refresh live status
|
||||||
this->roomIdChanged.connect([this]() {
|
this->roomIdChanged.connect([this]() {
|
||||||
this->refreshPubSub();
|
this->refreshPubSub();
|
||||||
this->refreshTitle();
|
|
||||||
this->refreshLiveStatus();
|
|
||||||
this->refreshBadges();
|
this->refreshBadges();
|
||||||
this->refreshCheerEmotes();
|
this->refreshCheerEmotes();
|
||||||
this->refreshFFZChannelEmotes(false);
|
this->refreshFFZChannelEmotes(false);
|
||||||
this->refreshBTTVChannelEmotes(false);
|
this->refreshBTTVChannelEmotes(false);
|
||||||
this->refreshSevenTVChannelEmotes(false);
|
this->refreshSevenTVChannelEmotes(false);
|
||||||
this->joinBttvChannel();
|
this->joinBttvChannel();
|
||||||
|
getIApp()->getTwitchLiveController()->add(
|
||||||
|
std::dynamic_pointer_cast<TwitchChannel>(shared_from_this()));
|
||||||
});
|
});
|
||||||
|
|
||||||
this->connected.connect([this]() {
|
this->connected.connect([this]() {
|
||||||
@@ -151,6 +152,85 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||||||
});
|
});
|
||||||
this->threadClearTimer_.start(5 * 60 * 1000);
|
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
|
// debugging
|
||||||
#if 0
|
#if 0
|
||||||
for (int i = 0; i < 1000; i++) {
|
for (int i = 0; i < 1000; i++) {
|
||||||
@@ -172,7 +252,6 @@ TwitchChannel::~TwitchChannel()
|
|||||||
|
|
||||||
void TwitchChannel::initialize()
|
void TwitchChannel::initialize()
|
||||||
{
|
{
|
||||||
this->fetchDisplayName();
|
|
||||||
this->refreshChatters();
|
this->refreshChatters();
|
||||||
this->refreshBadges();
|
this->refreshBadges();
|
||||||
}
|
}
|
||||||
@@ -340,6 +419,89 @@ boost::optional<ChannelPointReward> TwitchChannel::channelPointReward(
|
|||||||
return it->second;
|
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()
|
void TwitchChannel::showLoginMessage()
|
||||||
{
|
{
|
||||||
const auto linkColor = MessageColor(MessageColor::Link);
|
const auto linkColor = MessageColor(MessageColor::Link);
|
||||||
@@ -909,183 +1071,16 @@ int TwitchChannel::chatterCount()
|
|||||||
return this->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();
|
return false;
|
||||||
if (guard->live == newLiveStatus)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
guard->live = newLiveStatus;
|
|
||||||
}
|
}
|
||||||
|
guard->live = newLiveStatus;
|
||||||
|
|
||||||
if (newLiveStatus)
|
return true;
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TwitchChannel::loadRecentMessages()
|
void TwitchChannel::loadRecentMessages()
|
||||||
@@ -1220,33 +1215,6 @@ void TwitchChannel::refreshChatters()
|
|||||||
[](auto error, auto message) {});
|
[](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)
|
void TwitchChannel::addReplyThread(const std::shared_ptr<MessageThread> &thread)
|
||||||
{
|
{
|
||||||
this->threads_[thread->rootId()] = thread;
|
this->threads_[thread->rootId()] = thread;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
@@ -120,7 +121,6 @@ public:
|
|||||||
virtual bool hasHighRateLimit() const override;
|
virtual bool hasHighRateLimit() const override;
|
||||||
virtual bool canReconnect() const override;
|
virtual bool canReconnect() const override;
|
||||||
virtual void reconnect() override;
|
virtual void reconnect() override;
|
||||||
void refreshTitle();
|
|
||||||
void createClip();
|
void createClip();
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
@@ -194,7 +194,23 @@ public:
|
|||||||
// Signals
|
// Signals
|
||||||
pajlada::Signals::NoArgSignal roomIdChanged;
|
pajlada::Signals::NoArgSignal roomIdChanged;
|
||||||
pajlada::Signals::NoArgSignal userStateChanged;
|
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;
|
pajlada::Signals::NoArgSignal roomModesChanged;
|
||||||
|
|
||||||
// Channel point rewards
|
// Channel point rewards
|
||||||
@@ -205,6 +221,12 @@ public:
|
|||||||
boost::optional<ChannelPointReward> channelPointReward(
|
boost::optional<ChannelPointReward> channelPointReward(
|
||||||
const QString &rewardId) const;
|
const QString &rewardId) const;
|
||||||
|
|
||||||
|
// Live status
|
||||||
|
void updateStreamStatus(const std::optional<HelixStream> &helixStream);
|
||||||
|
void updateStreamTitle(const QString &title);
|
||||||
|
|
||||||
|
void updateDisplayName(const QString &displayName);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct NameOptions {
|
struct NameOptions {
|
||||||
QString displayName;
|
QString displayName;
|
||||||
@@ -213,21 +235,23 @@ private:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
// Methods
|
// Methods
|
||||||
void refreshLiveStatus();
|
|
||||||
void parseLiveStatus(bool live, const HelixStream &stream);
|
|
||||||
void refreshPubSub();
|
void refreshPubSub();
|
||||||
void refreshChatters();
|
void refreshChatters();
|
||||||
void refreshBadges();
|
void refreshBadges();
|
||||||
void refreshCheerEmotes();
|
void refreshCheerEmotes();
|
||||||
void loadRecentMessages();
|
void loadRecentMessages();
|
||||||
void loadRecentMessagesReconnect();
|
void loadRecentMessagesReconnect();
|
||||||
void fetchDisplayName();
|
|
||||||
void cleanUpReplyThreads();
|
void cleanUpReplyThreads();
|
||||||
void showLoginMessage();
|
void showLoginMessage();
|
||||||
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
|
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */
|
||||||
void joinBttvChannel() const;
|
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 setMod(bool value);
|
||||||
void setVIP(bool value);
|
void setVIP(bool value);
|
||||||
void setStaff(bool value);
|
void setStaff(bool value);
|
||||||
@@ -236,7 +260,16 @@ private:
|
|||||||
void setDisplayName(const QString &name);
|
void setDisplayName(const QString &name);
|
||||||
void setLocalizedName(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;
|
const QString &getDisplayName() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the localized name of the user
|
||||||
|
**/
|
||||||
const QString &getLocalizedName() const override;
|
const QString &getLocalizedName() const override;
|
||||||
|
|
||||||
QString prepareMessage(const QString &message) const;
|
QString prepareMessage(const QString &message) const;
|
||||||
|
|||||||
@@ -80,12 +80,6 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
|||||||
this->reloadBTTVGlobalEmotes();
|
this->reloadBTTVGlobalEmotes();
|
||||||
this->reloadFFZGlobalEmotes();
|
this->reloadFFZGlobalEmotes();
|
||||||
this->reloadSevenTVGlobalEmotes();
|
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,
|
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
||||||
@@ -412,59 +406,6 @@ std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
|
|||||||
return Channel::getEmpty();
|
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)
|
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
|
||||||
{
|
{
|
||||||
if (dirtyChannelName.startsWith('#'))
|
if (dirtyChannelName.startsWith('#'))
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ public:
|
|||||||
|
|
||||||
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
|
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
|
||||||
|
|
||||||
void bulkRefreshLiveStatus();
|
|
||||||
|
|
||||||
void reloadBTTVGlobalEmotes();
|
void reloadBTTVGlobalEmotes();
|
||||||
void reloadAllBTTVChannelEmotes();
|
void reloadAllBTTVChannelEmotes();
|
||||||
void reloadFFZGlobalEmotes();
|
void reloadFFZGlobalEmotes();
|
||||||
@@ -124,7 +122,6 @@ private:
|
|||||||
BttvEmotes bttv;
|
BttvEmotes bttv;
|
||||||
FfzEmotes ffz;
|
FfzEmotes ffz;
|
||||||
SeventvEmotes seventv_;
|
SeventvEmotes seventv_;
|
||||||
QTimer bulkLiveStatusTimer_;
|
|
||||||
|
|
||||||
pajlada::Signals::SignalHolder signalHolder_;
|
pajlada::Signals::SignalHolder signalHolder_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -417,6 +417,45 @@ void Helix::createClip(QString channelId,
|
|||||||
.execute();
|
.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,
|
void Helix::getChannel(QString broadcasterId,
|
||||||
ResultCallback<HelixChannel> successCallback,
|
ResultCallback<HelixChannel> successCallback,
|
||||||
HelixFailureCallback failureCallback)
|
HelixFailureCallback failureCallback)
|
||||||
|
|||||||
@@ -788,6 +788,12 @@ public:
|
|||||||
std::function<void(HelixClipError)> failureCallback,
|
std::function<void(HelixClipError)> failureCallback,
|
||||||
std::function<void()> finallyCallback) = 0;
|
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
|
// https://dev.twitch.tv/docs/api/reference#get-channel-information
|
||||||
virtual void getChannel(QString broadcasterId,
|
virtual void getChannel(QString broadcasterId,
|
||||||
ResultCallback<HelixChannel> successCallback,
|
ResultCallback<HelixChannel> successCallback,
|
||||||
@@ -1101,6 +1107,12 @@ public:
|
|||||||
std::function<void(HelixClipError)> failureCallback,
|
std::function<void(HelixClipError)> failureCallback,
|
||||||
std::function<void()> finallyCallback) final;
|
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
|
// https://dev.twitch.tv/docs/api/reference#get-channel-information
|
||||||
void getChannel(QString broadcasterId,
|
void getChannel(QString broadcasterId,
|
||||||
ResultCallback<HelixChannel> successCallback,
|
ResultCallback<HelixChannel> successCallback,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams
|
|||||||
|
|
||||||
Used in:
|
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
|
- `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
|
||||||
|
|
||||||
### Create Clip
|
### Create Clip
|
||||||
@@ -61,7 +61,7 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information
|
|||||||
|
|
||||||
Used in:
|
Used in:
|
||||||
|
|
||||||
- `TwitchChannel` to refresh stream title
|
- `LiveController` to refresh stream title & display name
|
||||||
|
|
||||||
### Update Channel
|
### Update Channel
|
||||||
|
|
||||||
|
|||||||
@@ -800,7 +800,7 @@ void ChannelView::setChannel(ChannelPtr underlyingChannel)
|
|||||||
if (auto tc = dynamic_cast<TwitchChannel *>(underlyingChannel.get()))
|
if (auto tc = dynamic_cast<TwitchChannel *>(underlyingChannel.get()))
|
||||||
{
|
{
|
||||||
this->channelConnections_.managedConnect(
|
this->channelConnections_.managedConnect(
|
||||||
tc->liveStatusChanged, [this]() {
|
tc->streamStatusChanged, [this]() {
|
||||||
this->liveStatusChanged.invoke();
|
this->liveStatusChanged.invoke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -740,7 +740,7 @@ void SplitHeader::handleChannelChanged()
|
|||||||
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
if (auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||||
{
|
{
|
||||||
this->channelConnections_.managedConnect(
|
this->channelConnections_.managedConnect(
|
||||||
twitchChannel->liveStatusChanged, [this]() {
|
twitchChannel->streamStatusChanged, [this]() {
|
||||||
this->updateChannelText();
|
this->updateChannelText();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -956,10 +956,6 @@ void SplitHeader::enterEvent(QEvent *event)
|
|||||||
if (!this->tooltipText_.isEmpty())
|
if (!this->tooltipText_.isEmpty())
|
||||||
{
|
{
|
||||||
auto *channel = this->split_->getChannel().get();
|
auto *channel = this->split_->getChannel().get();
|
||||||
if (channel->getType() == Channel::Type::Twitch)
|
|
||||||
{
|
|
||||||
dynamic_cast<TwitchChannel *>(channel)->refreshTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *tooltip = TooltipWidget::instance();
|
auto *tooltip = TooltipWidget::instance();
|
||||||
tooltip->setOne({nullptr, this->tooltipText_});
|
tooltip->setOne({nullptr, this->tooltipText_});
|
||||||
|
|||||||
Reference in New Issue
Block a user