From a83d2d9910632415e8ea36902b9724541085003f Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Sun, 8 Oct 2023 00:14:29 +0200 Subject: [PATCH] Add channel selection --- src/macro-external/twitch/CMakeLists.txt | 2 + .../twitch/channel-selection.cpp | 217 ++++++++++++++++++ .../twitch/channel-selection.hpp | 81 +++++++ 3 files changed, 300 insertions(+) create mode 100644 src/macro-external/twitch/channel-selection.cpp create mode 100644 src/macro-external/twitch/channel-selection.hpp diff --git a/src/macro-external/twitch/CMakeLists.txt b/src/macro-external/twitch/CMakeLists.txt index 3663d32a..cbed769d 100644 --- a/src/macro-external/twitch/CMakeLists.txt +++ b/src/macro-external/twitch/CMakeLists.txt @@ -39,6 +39,8 @@ target_sources( ${PROJECT_NAME} PRIVATE category-selection.cpp category-selection.hpp + channel-selection.cpp + channel-selection.hpp macro-action-twitch.cpp macro-action-twitch.hpp token.cpp diff --git a/src/macro-external/twitch/channel-selection.cpp b/src/macro-external/twitch/channel-selection.cpp new file mode 100644 index 00000000..042d5af1 --- /dev/null +++ b/src/macro-external/twitch/channel-selection.cpp @@ -0,0 +1,217 @@ +#include "channel-selection.hpp" +#include "twitch-helpers.hpp" +#include "token.hpp" + +#include +#include +#include + +namespace advss { + +bool ChannelLiveInfo::IsLive() const +{ + return type == "live"; +} + +void TwitchChannel::Load(obs_data_t *obj) +{ + OBSDataAutoRelease data = obs_data_get_obj(obj, "channel"); + _name.Load(data, "name"); +} + +void TwitchChannel::Save(obs_data_t *obj) const +{ + OBSDataAutoRelease data = obs_data_create(); + _name.Save(data, "name"); + obs_data_set_obj(obj, "channel", data); +} + +std::string TwitchChannel::GetUserID(const TwitchToken &token) +{ + static std::map userIDCache; + auto it = userIDCache.find(std::string(_name)); + if (it != userIDCache.end()) { + return it->second; + } + + auto res = SendGetRequest("https://api.twitch.tv", "/helix/users", + token, {{"login", _name}}); + + if (res.status == 400) { + userIDCache[_name] = "invalid"; + blog(LOG_INFO, "ignoring invalid channel selection '%s'!", + _name.c_str()); + return "invalid"; + } + + if (res.status != 200) { + blog(LOG_INFO, "failed to get Twitch user id from token! (%d)", + res.status); + return "invalid"; + } + OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data"); + size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + OBSDataAutoRelease arrayObj = obs_data_array_item(array, i); + std::string id = obs_data_get_string(arrayObj, "id"); + userIDCache[_name] = id; + return id; + } + userIDCache[_name] = "invalid"; + return "invalid"; +} + +static QDate parseRFC3339Date(const std::string &rfc3339Date) +{ + QString rfc3339QString = QString::fromStdString(rfc3339Date); + QRegularExpression regex("(\\d{4})-(\\d{2})-(\\d{2})"); + QRegularExpressionMatch match = regex.match(rfc3339QString); + + if (match.hasMatch()) { + int year = match.captured(1).toInt(); + int month = match.captured(2).toInt(); + int day = match.captured(3).toInt(); + QDate date(year, month, day); + return date; + } else { + return QDate(); + } +} + +static std::vector +getStringArrayHelper(const OBSDataAutoRelease &data, const std::string &value) +{ + static std::vector result; + OBSDataArrayAutoRelease array = obs_data_get_array(data, value.c_str()); + size_t count = obs_data_array_count(array); + for (size_t i = 0; i < count; i++) { + // TODO + } + return result; +} + +std::optional +TwitchChannel::GetLiveInfo(const TwitchToken &token) +{ + auto id = GetUserID(token); + if (!IsValid(id)) { + return {}; + } + + httplib::Params params = { + {"first", "1"}, {"after", ""}, {"user_id", id}}; + auto result = SendGetRequest("https://api.twitch.tv", "/helix/streams", + token, params, true); + if (result.status != 200) { + return {}; + } + + OBSDataArrayAutoRelease array = obs_data_get_array(result.data, "data"); + size_t count = obs_data_array_count(array); + if (count == 0) { + return {}; + } + OBSDataAutoRelease data = obs_data_array_item(array, 0); + + ChannelLiveInfo info; + info.id = obs_data_get_string(data, "id"); + info.user_id = obs_data_get_string(data, "user_id"); + info.user_login = obs_data_get_string(data, "user_login"); + info.user_name = obs_data_get_string(data, "user_name"); + info.game_id = obs_data_get_string(data, "game_id"); + info.game_name = obs_data_get_string(data, "game_name"); + info.type = obs_data_get_string(data, "type"); + info.title = obs_data_get_string(data, "title"); + info.tags = getStringArrayHelper(data, "tags"); + info.viewer_count = obs_data_get_int(data, "viewer_count"); + info.started_at = + parseRFC3339Date(obs_data_get_string(data, "started_at")); + info.language = obs_data_get_string(data, "language"); + info.thumbnail_url = obs_data_get_string(data, "thumbnail_url"); + info.is_mature = obs_data_get_bool(data, "is_mature"); + return info; +} + +std::optional TwitchChannel::GetInfo(const TwitchToken &token) +{ + auto id = GetUserID(token); + if (!IsValid(id)) { + return {}; + } + + httplib::Params params = { + {"first", "1"}, {"after", ""}, {"broadcaster_id", id}}; + auto result = SendGetRequest("https://api.twitch.tv", "/helix/channels", + token, params, true); + if (result.status != 200) { + return {}; + } + + OBSDataArrayAutoRelease array = obs_data_get_array(result.data, "data"); + size_t count = obs_data_array_count(array); + if (count == 0) { + return {}; + } + OBSDataAutoRelease data = obs_data_array_item(array, 0); + + ChannelInfo info; + info.broadcaster_id = obs_data_get_string(data, "broadcaster_id"); + info.broadcaster_login = obs_data_get_string(data, "broadcaster_login"); + info.broadcaster_name = obs_data_get_string(data, "broadcaster_name"); + info.broadcaster_language = + obs_data_get_string(data, "broadcaster_language"); + info.game_name = obs_data_get_string(data, "game_name"); + info.game_id = obs_data_get_string(data, "game_id"); + info.title = obs_data_get_string(data, "title"); + info.delay = obs_data_get_int(data, "delay"); + info.tags = getStringArrayHelper(data, "tags"); + info.content_classification_labels = + getStringArrayHelper(data, "thumbnail_url"); + info.is_branded_content = obs_data_get_bool(data, "is_branded_content"); + return info; +} + +bool TwitchChannel::IsValid(const std::string &id) const +{ + return id != "invalid" && !id.empty(); +} + +TwitchChannelSelection::TwitchChannelSelection(QWidget *parent) + : QWidget(parent), + _channelName(new VariableLineEdit(this)), + _openChannel(new QPushButton( + obs_module_text("AdvSceneSwitcher.channel.open"))) +{ + QWidget::connect(_channelName, SIGNAL(editingFinished()), this, + SLOT(SelectionChanged())); + QWidget::connect(_openChannel, SIGNAL(pressed()), this, + SLOT(OpenChannel())); + + auto layout = new QHBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(_channelName); + layout->addWidget(_openChannel); + setLayout(layout); +} + +void TwitchChannelSelection::SetChannel(const TwitchChannel &channel) +{ + _channelName->setText(channel._name); +} + +void TwitchChannelSelection::OpenChannel() +{ + // Resolve potential variables + StringVariable temp = _channelName->text().toStdString(); + QDesktopServices::openUrl("https://www.twitch.tv/" + + QString::fromStdString(temp)); +} + +void TwitchChannelSelection::SelectionChanged() +{ + TwitchChannel channel; + channel._name = _channelName->text().toStdString(); + emit ChannelChanged(channel); +} + +} // namespace advss diff --git a/src/macro-external/twitch/channel-selection.hpp b/src/macro-external/twitch/channel-selection.hpp new file mode 100644 index 00000000..52b5a371 --- /dev/null +++ b/src/macro-external/twitch/channel-selection.hpp @@ -0,0 +1,81 @@ +#pragma once +#include +#include +#include +#include + +namespace advss { + +class TwitchToken; + +// Information returned by the https://api.twitch.tv/helix/streams API call +// Channel must be live +struct ChannelLiveInfo { + bool IsLive() const; + + std::string id; + std::string user_id; + std::string user_login; + std::string user_name; + std::string game_id; + std::string game_name; + std::string type; + std::string title; + std::vector tags; + int viewer_count; + QDate started_at; + std::string language; + std::string thumbnail_url; + bool is_mature; +}; + +// Information returned by the https://api.twitch.tv/helix/channels API call +// Channel can be offline +struct ChannelInfo { + std::string broadcaster_id; + std::string broadcaster_login; + std::string broadcaster_name; + std::string broadcaster_language; + std::string game_name; + std::string game_id; + std::string title; + uint32_t delay; + std::vector tags; + std::vector content_classification_labels; + bool is_branded_content; +}; + +struct TwitchChannel { + void Load(obs_data_t *obj); + void Save(obs_data_t *obj) const; + std::string GetUserID(const TwitchToken &); + bool IsValid(const std::string &id) const; + std::optional GetLiveInfo(const TwitchToken &); + std::optional GetInfo(const TwitchToken &); + +private: + StringVariable _name = ""; + + friend class TwitchChannelSelection; +}; + +class TwitchChannelSelection : public QWidget { + Q_OBJECT + +public: + TwitchChannelSelection(QWidget *parent); + void SetChannel(const TwitchChannel &); + +private slots: + void SelectionChanged(); + void OpenChannel(); + +signals: + void ChannelChanged(const TwitchChannel &); + +private: + VariableLineEdit *_channelName; + QPushButton *_openChannel; +}; + +} // namespace advss