diff --git a/.gitignore b/.gitignore index 410ca6beb..602c33de2 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ Thumbs.db .settings .idea .metadata +.vscode *.iml *.ipr *.sublime* diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 1cae4792a..d8af0f640 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -779,10 +779,15 @@ 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.marker="Create stream marker" +AdvSceneSwitcher.action.twitch.type.clip="Create stream clip" 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}}{{duration}}" +AdvSceneSwitcher.action.twitch.entry="On{{account}}{{actions}}{{streamTitle}}{{category}}{{manualCategorySearch}}{{markerDescription}}{{clipHasDelay}}{{duration}}" AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!" +AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip" +AdvSceneSwitcher.action.twitch.marker.description="Describe marker, max 140 chars" +AdvSceneSwitcher.action.twitch.title.title="Enter title, max 140 chars" ; Hotkey AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher" @@ -935,6 +940,7 @@ AdvSceneSwitcher.twitchToken.analytics.readGames="View analytics data for the ga 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.twitchToken.channel.createClip="Create clips from channel's broadcasts." AdvSceneSwitcher.twitchCategories.fetchStart="Fetching stream categories ..." AdvSceneSwitcher.twitchCategories.fetchStatus="Got %1 stream categories." diff --git a/data/locale/fr-FR.ini b/data/locale/fr-FR.ini index 96fc8dce4..912d2f39c 100644 --- a/data/locale/fr-FR.ini +++ b/data/locale/fr-FR.ini @@ -737,9 +737,15 @@ AdvSceneSwitcher.action.sceneLock.entry="Sur {{scenes}} {{actions}} {{sources}}" AdvSceneSwitcher.action.twitch="Twitch" AdvSceneSwitcher.action.twitch.type.title="Définir le titre du flux" AdvSceneSwitcher.action.twitch.type.category="Définir la catégorie du flux" +AdvSceneSwitcher.action.twitch.type.marker="Create stream marker" +AdvSceneSwitcher.action.twitch.type.clip="Create stream clip" AdvSceneSwitcher.action.twitch.type.commercial="Démarrer une publicité d'une durée de" AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Impossible de sélectionner une catégorie sans avoir d'abord sélectionné un compte Twitch !" -AdvSceneSwitcher.action.twitch.entry="Sur {{account}} {{actions}} {{text}} {{category}} {{manualCategorySearch}} {{duration}}" +AdvSceneSwitcher.action.twitch.entry="Sur {{account}} {{actions}} {{streamTitle}} {{category}} {{manualCategorySearch}} {{markerDescription}} {{clipHasDelay}} {{duration}}" +AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient="Permissions of selected token are insufficient to perform selected action!" +AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip" +AdvSceneSwitcher.action.twitch.marker.description="Describe marker, max 140 chars" +AdvSceneSwitcher.action.twitch.title.title="Enter title, max 140 chars" ; Transition Tab AdvSceneSwitcher.transitionTab.title="Transition" @@ -1121,6 +1127,7 @@ AdvSceneSwitcher.twitchToken.analytics.readGames="Afficher les données d'analys AdvSceneSwitcher.twitchToken.bits.read="Afficher les informations sur les Bits pour une chaîne." AdvSceneSwitcher.twitchToken.channel.manageBroadcast="Gérer la configuration de diffusion d'une chaîne, y compris la mise à jour de la configuration de la chaîne et la gestion des marqueurs de diffusion en continu et des balises de diffusion en continu." AdvSceneSwitcher.twitchToken.channel.startCommercial="Lancer des publicités sur une chaîne." +AdvSceneSwitcher.twitchToken.channel.createClip="Create clips from channel's broadcasts." AdvSceneSwitcher.twitchCategories.fetchStart="Récupération des catégories de streams..." AdvSceneSwitcher.twitchCategories.fetchStatus="Obtenu %1 catégories de streams." diff --git a/src/macro-external/twitch/macro-action-twitch.cpp b/src/macro-external/twitch/macro-action-twitch.cpp index 0dcbf30f1..d06478506 100644 --- a/src/macro-external/twitch/macro-action-twitch.cpp +++ b/src/macro-external/twitch/macro-action-twitch.cpp @@ -18,6 +18,10 @@ const static std::map actionTypes = { "AdvSceneSwitcher.action.twitch.type.title"}, {MacroActionTwitch::Action::CATEGORY, "AdvSceneSwitcher.action.twitch.type.category"}, + {MacroActionTwitch::Action::MARKER, + "AdvSceneSwitcher.action.twitch.type.marker"}, + {MacroActionTwitch::Action::CLIP, + "AdvSceneSwitcher.action.twitch.type.clip"}, {MacroActionTwitch::Action::COMMERCIAL, "AdvSceneSwitcher.action.twitch.type.commercial"}, }; @@ -25,17 +29,18 @@ const static std::map actionTypes = { void MacroActionTwitch::SetStreamTitle( const std::shared_ptr &token) const { - if (std::string(_text).empty()) { + if (std::string(_streamTitle).empty()) { return; } OBSDataAutoRelease data = obs_data_create(); - obs_data_set_string(data, "title", _text.c_str()); + obs_data_set_string(data, "title", _streamTitle.c_str()); auto result = SendPatchRequest( "https://api.twitch.tv", std::string("/helix/channels?broadcaster_id=") + token->GetUserID(), *token, data.Get()); + if (result.status != 204) { blog(LOG_INFO, "Failed to set stream title! (%d)", result.status); @@ -63,6 +68,39 @@ void MacroActionTwitch::SetStreamCategory( } } +void MacroActionTwitch::CreateStreamMarker( + const std::shared_ptr &token) const +{ + OBSDataAutoRelease data = obs_data_create(); + obs_data_set_string(data, "user_id", token->GetUserID().c_str()); + + if (!std::string(_markerDescription).empty()) { + obs_data_set_string(data, "description", _markerDescription.c_str()); + } + + auto result = SendPostRequest("https://api.twitch.tv", + "/helix/streams/markers", *token, data.Get()); + + if (result.status != 200) { + blog(LOG_INFO, "Failed to create marker! (%d)", result.status); + } +} + +void MacroActionTwitch::CreateStreamClip( + const std::shared_ptr &token) const +{ + OBSDataAutoRelease data = obs_data_create(); + auto hasDelay = _clipHasDelay ? "true" : "false"; + auto result = SendPostRequest("https://api.twitch.tv", + "/helix/clips?broadcaster_id=" + token->GetUserID() + + "&has_delay=" + hasDelay, + *token, data.Get()); + + if (result.status != 202) { + blog(LOG_INFO, "Failed to create clip! (%d)", result.status); + } +} + void MacroActionTwitch::StartCommercial( const std::shared_ptr &token) const { @@ -102,6 +140,12 @@ bool MacroActionTwitch::PerformAction() case MacroActionTwitch::Action::CATEGORY: SetStreamCategory(token); break; + case MacroActionTwitch::Action::MARKER: + CreateStreamMarker(token); + break; + case MacroActionTwitch::Action::CLIP: + CreateStreamClip(token); + break; case MacroActionTwitch::Action::COMMERCIAL: StartCommercial(token); break; @@ -131,8 +175,10 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const obs_data_set_int(obj, "action", static_cast(_action)); obs_data_set_string(obj, "token", GetWeakTwitchTokenName(_token).c_str()); - _text.Save(obj, "text"); + _streamTitle.Save(obj, "streamTitle"); _category.Save(obj); + _markerDescription.Save(obj, "markerDescription"); + obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay); _duration.Save(obj); return true; } @@ -142,8 +188,10 @@ bool MacroActionTwitch::Load(obs_data_t *obj) MacroAction::Load(obj); _action = static_cast(obs_data_get_int(obj, "action")); _token = GetWeakTwitchTokenByName(obs_data_get_string(obj, "token")); - _text.Load(obj, "text"); + _streamTitle.Load(obj, "streamTitle"); _category.Load(obj); + _markerDescription.Load(obj, "markerDescription"); + _clipHasDelay = obs_data_get_bool(obj, "clipHasDelay"); _duration.Load(obj); return true; } @@ -158,6 +206,8 @@ bool MacroActionTwitch::ActionIsSupportedByToken() static const std::unordered_map requiredOption = { {Action::TITLE, {"channel:manage:broadcast"}}, {Action::CATEGORY, {"channel:manage:broadcast"}}, + {Action::MARKER, {"channel:manage:broadcast"}}, + {Action::CLIP, {"clips:edit"}}, {Action::COMMERCIAL, {"channel:edit:commercial"}}, }; auto token = _token.lock(); @@ -181,15 +231,20 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( : QWidget(parent), _actions(new QComboBox()), _tokens(new TwitchConnectionSelection()), - _text(new VariableLineEdit(this)), + _streamTitle(new VariableLineEdit(this)), _category(new TwitchCategorySelection(this)), _manualCategorySearch(new TwitchCategorySearchButton()), + _markerDescription(new VariableLineEdit(this)), + _clipHasDelay(new QCheckBox( + obs_module_text("AdvSceneSwitcher.action.twitch.clip.hasDelay"))), _duration(new DurationSelection(this, false, 0)), _layout(new QHBoxLayout()), _tokenPermissionWarning(new QLabel(obs_module_text( "AdvSceneSwitcher.action.twitch.tokenPermissionsInsufficient"))) { - _text->setSizePolicy(QSizePolicy::MinimumExpanding, + _streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding, + QSizePolicy::Preferred); + _markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _duration->SpinBox()->setSuffix("s"); populateActionSelection(_actions); @@ -198,14 +253,15 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( SLOT(ActionChanged(int))); QWidget::connect(_tokens, SIGNAL(SelectionChanged(const QString &)), this, SLOT(TwitchTokenChanged(const QString &))); - QWidget::connect(_text, SIGNAL(editingFinished()), this, - SLOT(TextChanged())); + QWidget::connect(_streamTitle, SIGNAL(editingFinished()), this, + SLOT(StreamTitleChanged())); QWidget::connect(_category, SIGNAL(CategoreyChanged(const TwitchCategory &)), this, SLOT(CategoreyChanged(const TwitchCategory &))); - QWidget::connect(_duration, - SIGNAL(CategoreyChanged(const TwitchCategory &)), this, - SLOT(CategoreyChanged(const TwitchCategory &))); + QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this, + SLOT(MarkerDescriptionChanged())); + QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this, + SLOT(HasClipDelayChanged(const Duration &))); QObject::connect(_duration, SIGNAL(DurationChanged(const Duration &)), this, SLOT(DurationChanged(const Duration &))); QWidget::connect(&_tokenPermissionCheckTimer, SIGNAL(timeout()), this, @@ -215,9 +271,11 @@ MacroActionTwitchEdit::MacroActionTwitchEdit( _layout, {{"{{account}}", _tokens}, {"{{actions}}", _actions}, - {"{{text}}", _text}, + {"{{streamTitle}}", _streamTitle}, {"{{category}}", _category}, {"{{manualCategorySearch}}", _manualCategorySearch}, + {"{{markerDescription}}", _markerDescription}, + {"{{clipHasDelay}}", _clipHasDelay}, {"{{duration}}", _duration}}); _layout->setContentsMargins(0, 0, 0, 0); @@ -247,14 +305,14 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token) emit(HeaderInfoChanged(token)); } -void MacroActionTwitchEdit::TextChanged() +void MacroActionTwitchEdit::StreamTitleChanged() { if (_loading || !_entryData) { return; } auto lock = LockContext(); - _entryData->_text = _text->text().toStdString(); + _entryData->_streamTitle = _streamTitle->text().toStdString(); } void MacroActionTwitchEdit::CategoreyChanged(const TwitchCategory &category) @@ -267,6 +325,26 @@ void MacroActionTwitchEdit::CategoreyChanged(const TwitchCategory &category) _entryData->_category = category; } +void MacroActionTwitchEdit::MarkerDescriptionChanged() +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_markerDescription = _markerDescription->text().toStdString(); +} + +void MacroActionTwitchEdit::ClipHasDelayChanged(int state) +{ + if (_loading || !_entryData) { + return; + } + + auto lock = LockContext(); + _entryData->_clipHasDelay = state; +} + void MacroActionTwitchEdit::DurationChanged(const Duration &duration) { if (_loading || !_entryData) { @@ -287,15 +365,21 @@ void MacroActionTwitchEdit::CheckTokenPermissions() void MacroActionTwitchEdit::SetupWidgetVisibility() { - _text->setVisible(_entryData->_action == - MacroActionTwitch::Action::TITLE); + _streamTitle->setVisible(_entryData->_action == + MacroActionTwitch::Action::TITLE); _category->setVisible(_entryData->_action == - MacroActionTwitch::Action::CATEGORY); + MacroActionTwitch::Action::CATEGORY); _manualCategorySearch->setVisible(_entryData->_action == - MacroActionTwitch::Action::CATEGORY); + MacroActionTwitch::Action::CATEGORY); + _markerDescription->setVisible(_entryData->_action == + MacroActionTwitch::Action::MARKER); + _clipHasDelay->setVisible(_entryData->_action == + MacroActionTwitch::Action::CLIP); _duration->setVisible(_entryData->_action == - MacroActionTwitch::Action::COMMERCIAL); - if (_entryData->_action == MacroActionTwitch::Action::TITLE) { + MacroActionTwitch::Action::COMMERCIAL); + + if (_entryData->_action == MacroActionTwitch::Action::TITLE + || _entryData->_action == MacroActionTwitch::Action::MARKER) { RemoveStretchIfPresent(_layout); } else { AddStretchIfNecessary(_layout); @@ -316,10 +400,12 @@ void MacroActionTwitchEdit::UpdateEntryData() _actions->setCurrentIndex(static_cast(_entryData->_action)); _tokens->SetToken(_entryData->_token); - _text->setText(_entryData->_text); + _streamTitle->setText(_entryData->_streamTitle); _category->SetToken(_entryData->_token); _manualCategorySearch->SetToken(_entryData->_token); _category->SetCategory(_entryData->_category); + _markerDescription->setText(_entryData->_markerDescription); + _clipHasDelay->setChecked(_entryData->_clipHasDelay); _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 f767e8b00..4bb9847d4 100644 --- a/src/macro-external/twitch/macro-action-twitch.hpp +++ b/src/macro-external/twitch/macro-action-twitch.hpp @@ -26,18 +26,26 @@ class MacroActionTwitch : public MacroAction { enum class Action { TITLE, CATEGORY, + MARKER, + CLIP, COMMERCIAL, }; Action _action = Action::TITLE; std::weak_ptr _token; - StringVariable _text = obs_module_text("AdvSceneSwitcher.enterText"); + StringVariable _streamTitle = + obs_module_text("AdvSceneSwitcher.action.twitch.title.title"); TwitchCategory _category; + StringVariable _markerDescription = + obs_module_text("AdvSceneSwitcher.action.twitch.marker.description"); + bool _clipHasDelay = false; Duration _duration = 60; private: void SetStreamTitle(const std::shared_ptr &) const; void SetStreamCategory(const std::shared_ptr &) const; + void CreateStreamMarker(const std::shared_ptr &) const; + void CreateStreamClip(const std::shared_ptr &) const; void StartCommercial(const std::shared_ptr &) const; static bool _registered; @@ -63,8 +71,10 @@ class MacroActionTwitchEdit : public QWidget { private slots: void ActionChanged(int); void TwitchTokenChanged(const QString &); - void TextChanged(); + void StreamTitleChanged(); void CategoreyChanged(const TwitchCategory &); + void MarkerDescriptionChanged(); + void ClipHasDelayChanged(int state); void DurationChanged(const Duration &); void CheckTokenPermissions(); @@ -79,9 +89,11 @@ private slots: QComboBox *_actions; TwitchConnectionSelection *_tokens; - VariableLineEdit *_text; + VariableLineEdit *_streamTitle; TwitchCategorySelection *_category; TwitchCategorySearchButton *_manualCategorySearch; + VariableLineEdit *_markerDescription; + QCheckBox *_clipHasDelay; DurationSelection *_duration; QHBoxLayout *_layout; QLabel *_tokenPermissionWarning; diff --git a/src/macro-external/twitch/token.cpp b/src/macro-external/twitch/token.cpp index ea6f10f54..7c8e40c1b 100644 --- a/src/macro-external/twitch/token.cpp +++ b/src/macro-external/twitch/token.cpp @@ -22,6 +22,7 @@ const std::unordered_map TokenOption::apiIdToLocale{ {"channel:manage:broadcast", "AdvSceneSwitcher.twitchToken.channel.manageBroadcast"}, + {"clips:edit", "AdvSceneSwitcher.twitchToken.channel.createClip"}, {"channel:edit:commercial", "AdvSceneSwitcher.twitchToken.channel.startCommercial"}, };