diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index e9c830ce..51e42107 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -777,8 +777,9 @@ AdvSceneSwitcher.action.sceneLock.entry="On{{scenes}}{{actions}}{{sources}}" AdvSceneSwitcher.action.twitch="Twitch" AdvSceneSwitcher.action.twitch.type.title="Set stream title" AdvSceneSwitcher.action.twitch.type.category="Set stream category" +AdvSceneSwitcher.action.twitch.type.commercial="Start commercial with duration" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!" -AdvSceneSwitcher.action.twitch.entry="On{{account}}{{actions}}{{text}}{{category}}{{manualCategorySearch}}" +AdvSceneSwitcher.action.twitch.entry="On{{account}}{{actions}}{{text}}{{category}}{{manualCategorySearch}}{{duration}}" ; Transition Tab AdvSceneSwitcher.transitionTab.title="Transition" @@ -1163,6 +1164,7 @@ AdvSceneSwitcher.twitchToken.analytics.readExtensions="View analytics data for t AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the games owned by the authenticated account." AdvSceneSwitcher.twitchToken.bits.read="View Bits information for a channel." AdvSceneSwitcher.twitchToken.channel.manageBroadcast="Manage a channel’s broadcast configuration, including updating channel configuration and managing stream markers and stream tags." +AdvSceneSwitcher.twitchToken.channel.startCommercial="Run commercials on a channel." AdvSceneSwitcher.twitchCategories.fetchStart="Fetching stream categories ..." AdvSceneSwitcher.twitchCategories.fetchStatus="Got %1 stream categories." diff --git a/src/macro-external/twitch/macro-action-twitch.cpp b/src/macro-external/twitch/macro-action-twitch.cpp index a63c19fe..c074f501 100644 --- a/src/macro-external/twitch/macro-action-twitch.cpp +++ b/src/macro-external/twitch/macro-action-twitch.cpp @@ -18,9 +18,12 @@ const static std::map actionTypes = { "AdvSceneSwitcher.action.twitch.type.title"}, {MacroActionTwitch::Action::CATEGORY, "AdvSceneSwitcher.action.twitch.type.category"}, + {MacroActionTwitch::Action::COMMERCIAL, + "AdvSceneSwitcher.action.twitch.type.commercial"}, }; -void MacroActionTwitch::SetStreamTitle(const std::shared_ptr &token) +void MacroActionTwitch::SetStreamTitle( + const std::shared_ptr &token) const { if (std::string(_text).empty()) { return; @@ -34,12 +37,13 @@ void MacroActionTwitch::SetStreamTitle(const std::shared_ptr &token token->GetUserID(), *token, data.Get()); if (result.status != 204) { - blog(LOG_INFO, "Failed to set stream title"); + blog(LOG_INFO, "Failed to set stream title! (%d)", + result.status); } } void MacroActionTwitch::SetStreamCategory( - const std::shared_ptr &token) + const std::shared_ptr &token) const { if (_category.id == -1) { return; @@ -54,7 +58,33 @@ void MacroActionTwitch::SetStreamCategory( token->GetUserID(), *token, data.Get()); if (result.status != 204) { - blog(LOG_INFO, "Failed to set stream category"); + blog(LOG_INFO, "Failed to set stream category! (%d)", + result.status); + } +} + +void MacroActionTwitch::StartCommercial( + const std::shared_ptr &token) const +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "broadcaster_id", token->GetUserID().c_str()); + obs_data_set_int(data, "length", _duration.Seconds()); + auto result = SendPostRequest("https://api.twitch.tv", + "/helix/channels/commercial", *token, + data.Get()); + if (result.status != 200) { + OBSDataArrayAutoRelease replyArray = + obs_data_get_array(result.data, "data"); + OBSDataAutoRelease replyData = + obs_data_array_item(replyArray, 0); + blog(LOG_INFO, + "Failed to start commercial! (%d)\n" + "length: %d\n" + "message: %s\n" + "retry_after: %d\n", + result.status, obs_data_get_int(replyData, "length"), + obs_data_get_string(replyData, "message"), + obs_data_get_int(replyData, "retry_after")); } } @@ -72,6 +102,9 @@ bool MacroActionTwitch::PerformAction() case MacroActionTwitch::Action::CATEGORY: SetStreamCategory(token); break; + case MacroActionTwitch::Action::COMMERCIAL: + StartCommercial(token); + break; default: break; } @@ -100,6 +133,7 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const GetWeakTwitchTokenName(_token).c_str()); _text.Save(obj, "text"); _category.Save(obj); + _duration.Save(obj); return true; } @@ -110,6 +144,7 @@ bool MacroActionTwitch::Load(obs_data_t *obj) _token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token")); _text.Load(obj, "text"); _category.Load(obj); + _duration.Load(obj); return true; } @@ -133,6 +168,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _text(new VariableLineEdit(this)), _category(new TwitchCategorySelection(this)), _manualCategorySearch(new TwitchCategorySearchButton()), + _duration(new DurationSelection(this, false, 0)), _layout(new QHBoxLayout()) { _text->setSizePolicy(QSizePolicy::MinimumExpanding, @@ -148,6 +184,11 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( QWidget::connect(_category, SIGNAL(CategoreyChanged(const TwitchCategory &)), this, SLOT(CategoreyChanged(const TwitchCategory &))); + QWidget::connect(_duration, + SIGNAL(CategoreyChanged(const TwitchCategory &)), this, + SLOT(CategoreyChanged(const TwitchCategory &))); + QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)), + this, SLOT(DurationChanged(const Duration &))); PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.twitch.entry"), _layout, @@ -155,7 +196,8 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( {"{{actions}}", _actions}, {"{{text}}", _text}, {"{{category}}", _category}, - {"{{manualCategorySearch}}", _manualCategorySearch}}); + {"{{manualCategorySearch}}", _manualCategorySearch}, + {"{{duration}}", _duration}}); setLayout(_layout); _entryData = entryData; @@ -197,12 +239,26 @@ void MacroActionTwitchEdit::CategoreyChanged(const TwitchCategory &category) _entryData->_category = category; } +void MacroActionTwitchEdit::DurationChanged(const Duration &duration) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_duration = duration; +} + void MacroActionTwitchEdit::SetupWidgetVisibility() { _text->setVisible(_entryData->_action == MacroActionTwitch::Action::TITLE); _category->setVisible(_entryData->_action == MacroActionTwitch::Action::CATEGORY); + _manualCategorySearch->setVisible(_entryData->_action == + MacroActionTwitch::Action::CATEGORY); + _duration->setVisible(_entryData->_action == + MacroActionTwitch::Action::COMMERCIAL); if (_entryData->_action == MacroActionTwitch::Action::TITLE) { RemoveStretchIfPresent(_layout); } else { @@ -225,6 +281,7 @@ void MacroActionTwitchEdit::UpdateEntryData() _category->SetToken(_entryData->_token); _manualCategorySearch->SetToken(_entryData->_token); _category->SetCategory(_entryData->_category); + _duration->SetDuration(_entryData->_duration); SetupWidgetVisibility(); } diff --git a/src/macro-external/twitch/macro-action-twitch.hpp b/src/macro-external/twitch/macro-action-twitch.hpp index 85062351..3aedca40 100644 --- a/src/macro-external/twitch/macro-action-twitch.hpp +++ b/src/macro-external/twitch/macro-action-twitch.hpp @@ -4,6 +4,7 @@ #include "category-selection.hpp" #include +#include namespace advss { @@ -24,16 +25,19 @@ public: enum class Action { TITLE, CATEGORY, + COMMERCIAL, }; Action _action = Action::TITLE; std::weak_ptr _token; StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText"); TwitchCategory _category; + Duration _duration = 60; private: - void SetStreamTitle(const std::shared_ptr &); - void SetStreamCategory(const std::shared_ptr &); + void SetStreamTitle(const std::shared_ptr &) const; + void SetStreamCategory(const std::shared_ptr &) const; + void StartCommercial(const std::shared_ptr &) const; static bool _registered; static const std::string id; @@ -60,6 +64,7 @@ private slots: void TwitchTokenChanged(const QString &); void TextChanged(); void CategoreyChanged(const TwitchCategory &); + void DurationChanged(const Duration &); signals: void HeaderInfoChanged(const QString &); @@ -75,6 +80,7 @@ private: VariableLineEdit *_text; TwitchCategorySelection *_category; TwitchCategorySearchButton *_manualCategorySearch; + DurationSelection *_duration; QHBoxLayout *_layout; bool _loading = true; }; diff --git a/src/macro-external/twitch/token.cpp b/src/macro-external/twitch/token.cpp index 4538150f..91d0a35c 100644 --- a/src/macro-external/twitch/token.cpp +++ b/src/macro-external/twitch/token.cpp @@ -22,6 +22,8 @@ const std::unordered_map TokenOption::apiIdToLocale{ {"channel:manage:broadcast", "AdvSceneSwitcher.twitchToken.channel.manageBroadcast"}, + {"channel:edit:commercial", + "AdvSceneSwitcher.twitchToken.channel.startCommercial"}, }; static void saveConnections(obs_data_t *obj); diff --git a/src/macro-external/twitch/twitch-helpers.cpp b/src/macro-external/twitch/twitch-helpers.cpp index 931ba791..7bff5573 100644 --- a/src/macro-external/twitch/twitch-helpers.cpp +++ b/src/macro-external/twitch/twitch-helpers.cpp @@ -32,12 +32,13 @@ RequestResult SendGetRequest(const std::string &uri, const std::string &path, } RequestResult SendPostRequest(const std::string &uri, const std::string &path, - const TwitchToken &token, - const httplib::Params ¶ms) + const TwitchToken &token, const OBSData &data) { httplib::Client cli(uri); auto headers = getTokenRequestHeaders(token); - auto response = cli.Post(path, headers, params); + auto json = obs_data_get_json(data); + std::string body = json ? json : ""; + auto response = cli.Post(path, headers, body, "application/json"); RequestResult result; result.status = response->status; if (response->body.empty()) { diff --git a/src/macro-external/twitch/twitch-helpers.hpp b/src/macro-external/twitch/twitch-helpers.hpp index d0f61bf4..7330b0ad 100644 --- a/src/macro-external/twitch/twitch-helpers.hpp +++ b/src/macro-external/twitch/twitch-helpers.hpp @@ -16,8 +16,7 @@ RequestResult SendGetRequest(const std::string &uri, const std::string &path, const TwitchToken &token, const httplib::Params & = {}); RequestResult SendPostRequest(const std::string &uri, const std::string &path, - const TwitchToken &token, - const httplib::Params ¶ms = {}); + const TwitchToken &token, const OBSData &data); RequestResult SendPatchRequest(const std::string &uri, const std::string &path, const TwitchToken &token, const OBSData &data); const char *GetClientID();