Add Twitch clip and marker actions

* Refactor stream title variable name
* Limit commerical duration to 180s
* Set maximum stream title size to 140 chars
This commit is contained in:
Przemek Pawlas 2023-09-15 22:56:57 +02:00 committed by WarmUpTill
parent 94416dd881
commit 107e1413ed
5 changed files with 146 additions and 25 deletions

View File

@ -780,10 +780,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"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
; Hotkey
AdvSceneSwitcher.hotkey.startSwitcherHotkey="Start the Advanced Scene Switcher"
@ -936,6 +941,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 channels 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."

View File

@ -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"
AdvSceneSwitcher.action.twitch.title.title="Enter title"
; 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."

View File

@ -18,6 +18,10 @@ const static std::map<MacroActionTwitch::Action, std::string> 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<MacroActionTwitch::Action, std::string> actionTypes = {
void MacroActionTwitch::SetStreamTitle(
const std::shared_ptr<TwitchToken> &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,42 @@ void MacroActionTwitch::SetStreamCategory(
}
}
void MacroActionTwitch::CreateStreamMarker(
const std::shared_ptr<TwitchToken> &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<TwitchToken> &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<TwitchToken> &token) const
{
@ -102,6 +143,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 +178,10 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const
obs_data_set_int(obj, "action", static_cast<int>(_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 +191,10 @@ bool MacroActionTwitch::Load(obs_data_t *obj)
MacroAction::Load(obj);
_action = static_cast<Action>(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 +209,8 @@ bool MacroActionTwitch::ActionIsSupportedByToken()
static const std::unordered_map<Action, TokenOption> 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,31 +234,42 @@ 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,
QSizePolicy::Preferred);
_duration->SpinBox()->setSuffix("s");
_streamTitle->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_streamTitle->setMaxLength(140);
_markerDescription->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::Preferred);
_markerDescription->setMaxLength(140);
auto spinBox = _duration->SpinBox();
spinBox->setSuffix("s");
spinBox->setMaximum(180);
populateActionSelection(_actions);
QWidget::connect(_actions, SIGNAL(currentIndexChanged(int)), this,
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 +279,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 +313,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 +333,27 @@ 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 +374,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);
_manualCategorySearch->setVisible(_entryData->_action ==
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) {
if (_entryData->_action == MacroActionTwitch::Action::TITLE ||
_entryData->_action == MacroActionTwitch::Action::MARKER) {
RemoveStretchIfPresent(_layout);
} else {
AddStretchIfNecessary(_layout);
@ -316,10 +409,12 @@ void MacroActionTwitchEdit::UpdateEntryData()
_actions->setCurrentIndex(static_cast<int>(_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();
}

View File

@ -26,18 +26,26 @@ public:
enum class Action {
TITLE,
CATEGORY,
MARKER,
CLIP,
COMMERCIAL,
};
Action _action = Action::TITLE;
std::weak_ptr<TwitchToken> _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<TwitchToken> &) const;
void SetStreamCategory(const std::shared_ptr<TwitchToken> &) const;
void CreateStreamMarker(const std::shared_ptr<TwitchToken> &) const;
void CreateStreamClip(const std::shared_ptr<TwitchToken> &) const;
void StartCommercial(const std::shared_ptr<TwitchToken> &) const;
static bool _registered;
@ -63,8 +71,10 @@ public:
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:
QComboBox *_actions;
TwitchConnectionSelection *_tokens;
VariableLineEdit *_text;
VariableLineEdit *_streamTitle;
TwitchCategorySelection *_category;
TwitchCategorySearchButton *_manualCategorySearch;
VariableLineEdit *_markerDescription;
QCheckBox *_clipHasDelay;
DurationSelection *_duration;
QHBoxLayout *_layout;
QLabel *_tokenPermissionWarning;

View File

@ -22,6 +22,7 @@ const std::unordered_map<std::string, std::string> TokenOption::apiIdToLocale{
{"channel:manage:broadcast",
"AdvSceneSwitcher.twitchToken.channel.manageBroadcast"},
{"clips:edit", "AdvSceneSwitcher.twitchToken.channel.createClip"},
{"channel:edit:commercial",
"AdvSceneSwitcher.twitchToken.channel.startCommercial"},
};