Add option to set content classification of Twitch stream

This commit is contained in:
WarmUpTill 2025-08-25 19:52:19 +02:00 committed by WarmUpTill
parent 4e561320f7
commit 4d22a539f0
6 changed files with 302 additions and 1 deletions

View File

@ -1242,6 +1242,14 @@ AdvSceneSwitcher.action.twitch.tags.duplicate="Duplicate Tag"
AdvSceneSwitcher.action.twitch.tags.duplicate.info="This tag is already in the list."
AdvSceneSwitcher.action.twitch.tags.limit="Tag Limit Reached"
AdvSceneSwitcher.action.twitch.tags.limit.info="You can only have up to %1 tags."
AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set="Set content classification labels"
AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics="Discussions or debates about politics or sensitive social issues such as elections, civic integrity, military conflict, and civil rights."
AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication="Excessive tobacco glorification or promotion, any marijuana consumption/use, legal drug and alcohol induced intoxication, discussions of illegal drugs."
AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes="Content that focuses on sexualized activities, sexual topics, or experiences."
AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic="Simulations and/or depictions of realistic violence, gore, extreme injury, or death."
AdvSceneSwitcher.action.twitch.contentClassification.gambling="Participating in or promoting online or in-person gambling, poker, or fantasy sports that involve the exchange of real money."
AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity="Prolonged and repeated use of obscenities, profanities, and vulgarities, especially as a regular part of speech."
AdvSceneSwitcher.action.twitch.contentClassification.getCurrent="Get current content classification labels"
AdvSceneSwitcher.action.clipboard="Clipboard"
AdvSceneSwitcher.action.clipboard.type.copy.text="Copy text"
AdvSceneSwitcher.action.clipboard.type.copy.image="Copy image"

View File

@ -86,6 +86,8 @@ target_sources(
chat-connection.hpp
chat-message-pattern.cpp
chat-message-pattern.hpp
content-classification.cpp
content-classification.hpp
event-sub.cpp
event-sub.hpp
macro-action-twitch.cpp

View File

@ -0,0 +1,206 @@
#include "content-classification.hpp"
#include "channel-selection.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "twitch-helpers.hpp"
#include <QPushButton>
#include <QVBoxLayout>
namespace advss {
void ContentClassification::Load(obs_data_t *obj)
{
OBSDataAutoRelease data =
obs_data_get_obj(obj, "contentClassificationLabels");
_debatedSocialIssuesAndPolitics =
obs_data_get_bool(data, "debatedSocialIssuesAndPolitics");
_drugsIntoxication = obs_data_get_bool(data, "drugsIntoxication");
_sexualThemes = obs_data_get_bool(data, "sexualThemes");
_violentGraphic = obs_data_get_bool(data, "violentGraphic");
_gambling = obs_data_get_bool(data, "gambling");
_profanityVulgarity = obs_data_get_bool(data, "profanityVulgarity");
}
void ContentClassification::Save(obs_data_t *obj) const
{
OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "debatedSocialIssuesAndPolitics",
_debatedSocialIssuesAndPolitics);
obs_data_set_bool(data, "drugsIntoxication", _drugsIntoxication);
obs_data_set_bool(data, "sexualThemes", _sexualThemes);
obs_data_set_bool(data, "violentGraphic", _violentGraphic);
obs_data_set_bool(data, "gambling", _gambling);
obs_data_set_bool(data, "profanityVulgarity", _profanityVulgarity);
obs_data_set_obj(obj, "contentClassificationLabels", data);
}
void ContentClassification::SetContentClassification(
const TwitchToken &token) const
{
OBSDataArrayAutoRelease ccls = obs_data_array_create();
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "id", "DebatedSocialIssuesAndPolitics");
obs_data_set_bool(data, "is_enabled", _debatedSocialIssuesAndPolitics);
obs_data_array_push_back(ccls, data);
data = obs_data_create();
obs_data_set_string(data, "id", "DrugsIntoxication");
obs_data_set_bool(data, "is_enabled", _drugsIntoxication);
obs_data_array_push_back(ccls, data);
data = obs_data_create();
obs_data_set_string(data, "id", "SexualThemes");
obs_data_set_bool(data, "is_enabled", _sexualThemes);
obs_data_array_push_back(ccls, data);
data = obs_data_create();
obs_data_set_string(data, "id", "ViolentGraphic");
obs_data_set_bool(data, "is_enabled", _violentGraphic);
obs_data_array_push_back(ccls, data);
data = obs_data_create();
obs_data_set_string(data, "id", "Gambling");
obs_data_set_bool(data, "is_enabled", _gambling);
obs_data_array_push_back(ccls, data);
data = obs_data_create();
obs_data_set_string(data, "id", "ProfanityVulgarity");
obs_data_set_bool(data, "is_enabled", _profanityVulgarity);
obs_data_array_push_back(ccls, data);
data = obs_data_create();
obs_data_set_array(data, "content_classification_labels", ccls);
auto result = SendPatchRequest(token, "https://api.twitch.tv",
"/helix/channels",
{{"broadcaster_id", token.GetUserID()}},
data.Get());
if (result.status != 204) {
blog(LOG_INFO,
"Failed to set stream content classification labels! (%d)",
result.status);
}
}
ContentClassificationEdit::ContentClassificationEdit(QWidget *parent)
: QWidget(parent),
_debatedSocialIssuesAndPolitics(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.debatedSocialIssuesAndPolitics"),
this)),
_drugsIntoxication(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.drugsIntoxication"),
this)),
_sexualThemes(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.sexualThemes"),
this)),
_violentGraphic(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.violentGraphic"),
this)),
_gambling(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.gambling"),
this)),
_profanityVulgarity(new QCheckBox(
obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.profanityVulgarity"),
this))
{
auto getCurrent = new QPushButton(obs_module_text(
"AdvSceneSwitcher.action.twitch.contentClassification.getCurrent"));
connect(getCurrent, &QPushButton::clicked, this,
&ContentClassificationEdit::GetCurrentClicked);
const auto emitChangeSignal = [this]() {
ContentClassification ccl;
ccl._debatedSocialIssuesAndPolitics =
_debatedSocialIssuesAndPolitics->isChecked();
ccl._drugsIntoxication = _drugsIntoxication->isChecked();
ccl._sexualThemes = _sexualThemes->isChecked();
ccl._violentGraphic = _violentGraphic->isChecked();
ccl._gambling = _gambling->isChecked();
ccl._profanityVulgarity = _profanityVulgarity->isChecked();
emit ContentClassificationChanged(ccl);
};
connect(_debatedSocialIssuesAndPolitics, &QCheckBox::stateChanged, this,
emitChangeSignal);
connect(_drugsIntoxication, &QCheckBox::stateChanged, this,
emitChangeSignal);
connect(_sexualThemes, &QCheckBox::stateChanged, this,
emitChangeSignal);
connect(_violentGraphic, &QCheckBox::stateChanged, this,
emitChangeSignal);
connect(_gambling, &QCheckBox::stateChanged, this, emitChangeSignal);
connect(_profanityVulgarity, &QCheckBox::stateChanged, this,
emitChangeSignal);
auto layout = new QVBoxLayout;
layout->addWidget(_debatedSocialIssuesAndPolitics);
layout->addWidget(_drugsIntoxication);
layout->addWidget(_sexualThemes);
layout->addWidget(_violentGraphic);
layout->addWidget(_gambling);
layout->addWidget(_profanityVulgarity);
layout->addWidget(getCurrent);
setLayout(layout);
// TODO:
// Figure out why the Twitch API always returns an empty CC list
// Hide for now
getCurrent->hide();
}
void ContentClassificationEdit::SetContentClassification(
const ContentClassification &ccl)
{
_debatedSocialIssuesAndPolitics->setChecked(
ccl._debatedSocialIssuesAndPolitics);
_drugsIntoxication->setChecked(ccl._drugsIntoxication);
_sexualThemes->setChecked(ccl._sexualThemes);
_violentGraphic->setChecked(ccl._violentGraphic);
_gambling->setChecked(ccl._gambling);
_profanityVulgarity->setChecked(ccl._profanityVulgarity);
}
void ContentClassificationEdit::SetToken(const std::weak_ptr<TwitchToken> &t)
{
_token = t;
}
void ContentClassificationEdit::GetCurrentClicked()
{
auto token = _token.lock();
if (!token) {
return;
}
TwitchChannel channel;
channel.SetName(token->Name());
const auto channelInfo = channel.GetInfo(*token);
if (!channelInfo) {
return;
}
ContentClassification ccl;
for (const auto &label : channelInfo->content_classification_labels) {
if (label == "DebatedSocialIssuesAndPolitics") {
ccl._debatedSocialIssuesAndPolitics = true;
} else if (label == "DrugsIntoxication") {
ccl._drugsIntoxication = true;
} else if (label == "SexualThemes") {
ccl._sexualThemes = true;
} else if (label == "ViolentGraphic") {
ccl._violentGraphic = true;
} else if (label == "Gambling") {
ccl._gambling = true;
} else if (label == "ProfanityVulgarity") {
ccl._profanityVulgarity = true;
}
}
SetContentClassification(ccl);
}
} // namespace advss

View File

@ -0,0 +1,50 @@
#pragma once
#include "token.hpp"
#include <QWidget>
class QCheckBox;
namespace advss {
struct ContentClassification {
void Load(obs_data_t *obj);
void Save(obs_data_t *obj) const;
void SetContentClassification(const TwitchToken &token) const;
private:
bool _debatedSocialIssuesAndPolitics = false;
bool _drugsIntoxication = false;
bool _sexualThemes = false;
bool _violentGraphic = false;
bool _gambling = false;
bool _profanityVulgarity = false;
friend class ContentClassificationEdit;
};
class ContentClassificationEdit final : public QWidget {
Q_OBJECT
public:
explicit ContentClassificationEdit(QWidget *parent = nullptr);
void SetContentClassification(const ContentClassification &ccl);
void SetToken(const std::weak_ptr<TwitchToken> &token);
private slots:
void GetCurrentClicked();
signals:
void ContentClassificationChanged(const ContentClassification &);
private:
QCheckBox *_debatedSocialIssuesAndPolitics;
QCheckBox *_drugsIntoxication;
QCheckBox *_sexualThemes;
QCheckBox *_violentGraphic;
QCheckBox *_gambling;
QCheckBox *_profanityVulgarity;
std::weak_ptr<TwitchToken> _token;
};
} // namespace advss

View File

@ -59,6 +59,8 @@ const static std::map<MacroActionTwitch::Action, std::string> actionTypes = {
"AdvSceneSwitcher.action.twitch.type.channel.info.category.set"},
{MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET,
"AdvSceneSwitcher.action.twitch.type.channel.info.tags.set"},
{MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET,
"AdvSceneSwitcher.action.twitch.type.channel.info.contentClassification.set"},
{MacroActionTwitch::Action::RAID_START,
"AdvSceneSwitcher.action.twitch.type.raid.start"},
{MacroActionTwitch::Action::COMMERCIAL_START,
@ -523,6 +525,9 @@ bool MacroActionTwitch::PerformAction()
case Action::CHANNEL_INFO_TAGS_SET:
_tags.SetStreamTags(*token);
break;
case Action::CHANNEL_INFO_CONTENT_LABELS_SET:
_contentClassification.SetContentClassification(*token);
break;
case Action::RAID_START:
StartRaid(token);
break;
@ -583,6 +588,7 @@ bool MacroActionTwitch::Save(obs_data_t *obj) const
_streamTitle.Save(obj, "streamTitle");
_category.Save(obj);
_tags.Save(obj);
_contentClassification.Save(obj);
_markerDescription.Save(obj, "markerDescription");
obs_data_set_bool(obj, "clipHasDelay", _clipHasDelay);
_duration.Save(obj);
@ -612,6 +618,7 @@ bool MacroActionTwitch::Load(obs_data_t *obj)
_streamTitle.Load(obj, "streamTitle");
_category.Load(obj);
_tags.Load(obj);
_contentClassification.Load(obj);
_markerDescription.Load(obj, "markerDescription");
_clipHasDelay = obs_data_get_bool(obj, "clipHasDelay");
_duration.Load(obj);
@ -652,6 +659,8 @@ bool MacroActionTwitch::ActionIsSupportedByToken()
{{"channel:manage:broadcast"}}},
{Action::CHANNEL_INFO_TAGS_SET,
{{"channel:manage:broadcast"}}},
{Action::CHANNEL_INFO_CONTENT_LABELS_SET,
{{"channel:manage:broadcast"}}},
{Action::CHANNEL_INFO_LANGUAGE_SET,
{{"channel:manage:broadcast"}}},
{Action::CHANNEL_INFO_DELAY_SET,
@ -826,6 +835,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
_streamTitle(new VariableLineEdit(this)),
_category(new TwitchCategoryWidget(this)),
_tags(new TagListWidget(this)),
_contentClassification(new ContentClassificationEdit(this)),
_markerDescription(new VariableLineEdit(this)),
_clipHasDelay(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.action.twitch.clip.hasDelay"))),
@ -852,6 +862,7 @@ MacroActionTwitchEdit::MacroActionTwitchEdit(
mainLayout->addWidget(_announcementMessage);
mainLayout->addWidget(_chatMessage);
mainLayout->addWidget(_tags);
mainLayout->addWidget(_contentClassification);
mainLayout->addWidget(_tokenWarning);
setLayout(mainLayout);
@ -889,6 +900,7 @@ void MacroActionTwitchEdit::TwitchTokenChanged(const QString &token)
_channel->SetToken(_entryData->_token);
_pointsReward->SetToken(_entryData->_token);
_tags->SetToken(_entryData->_token);
_contentClassification->SetToken(_entryData->_token);
_entryData->ResetChatConnection();
SetWidgetVisibility();
@ -949,6 +961,13 @@ void MacroActionTwitchEdit::TagsChanged(const TwitchTagList &tags)
_entryData->_tags = tags;
}
void MacroActionTwitchEdit::ContentClassificationChanged(
const ContentClassification &ccl)
{
GUARD_LOADING_AND_LOCK();
_entryData->_contentClassification = ccl;
}
void MacroActionTwitchEdit::MarkerDescriptionChanged()
{
GUARD_LOADING_AND_LOCK();
@ -1031,6 +1050,12 @@ void MacroActionTwitchEdit::SetWidgetSignalConnections()
SLOT(CategoryChanged(const TwitchCategory &)));
QWidget::connect(_tags, SIGNAL(TagListChanged(const TwitchTagList &)),
this, SLOT(TagsChanged(const TwitchTagList &)));
QWidget::connect(_contentClassification,
SIGNAL(ContentClassificationChanged(
const ContentClassification &)),
this,
SLOT(ContentClassificationChanged(
const ContentClassification &)));
QWidget::connect(_markerDescription, SIGNAL(editingFinished()), this,
SLOT(MarkerDescriptionChanged()));
QObject::connect(_clipHasDelay, SIGNAL(stateChanged(int)), this,
@ -1075,6 +1100,9 @@ void MacroActionTwitchEdit::SetWidgetVisibility()
MacroActionTwitch::Action::CHANNEL_INFO_CATEGORY_SET);
_tags->setVisible(_entryData->GetAction() ==
MacroActionTwitch::Action::CHANNEL_INFO_TAGS_SET);
_contentClassification->setVisible(
_entryData->GetAction() ==
MacroActionTwitch::Action::CHANNEL_INFO_CONTENT_LABELS_SET);
_channel->setVisible(
_entryData->GetAction() ==
MacroActionTwitch::Action::RAID_START ||
@ -1272,6 +1300,9 @@ void MacroActionTwitchEdit::UpdateEntryData()
_category->SetCategory(_entryData->_category);
_tags->SetTags(_entryData->_tags);
_tags->SetToken(_entryData->_token);
_contentClassification->SetContentClassification(
_entryData->_contentClassification);
_contentClassification->SetToken(_entryData->_token);
_markerDescription->setText(_entryData->_markerDescription);
_clipHasDelay->setChecked(_entryData->_clipHasDelay);
_duration->SetDuration(_entryData->_duration);

View File

@ -4,6 +4,7 @@
#include "category-selection.hpp"
#include "channel-selection.hpp"
#include "chat-connection.hpp"
#include "content-classification.hpp"
#include "points-reward-selection.hpp"
#include "tag-selection.hpp"
@ -29,7 +30,7 @@ public:
CHANNEL_INFO_LANGUAGE_SET = 30,
CHANNEL_INFO_DELAY_SET = 40,
CHANNEL_INFO_TAGS_SET = 50,
CHANNEL_INFO_CONTENT_LABELS_SET = 60, // TODO
CHANNEL_INFO_CONTENT_LABELS_SET = 60,
CHANNEL_INFO_BRANDED_CONTENT_ENABLE = 70,
CHANNEL_INFO_BRANDED_CONTENT_DISABLE = 71,
@ -170,6 +171,7 @@ public:
obs_module_text("AdvSceneSwitcher.action.twitch.title.title");
TwitchCategory _category;
TwitchTagList _tags;
ContentClassification _contentClassification;
StringVariable _markerDescription = obs_module_text(
"AdvSceneSwitcher.action.twitch.marker.description");
bool _clipHasDelay = false;
@ -238,6 +240,7 @@ private slots:
void StreamTitleChanged();
void CategoryChanged(const TwitchCategory &);
void TagsChanged(const TwitchTagList &tags);
void ContentClassificationChanged(const ContentClassification &ccl);
void MarkerDescriptionChanged();
void ClipHasDelayChanged(int state);
void DurationChanged(const Duration &);
@ -273,6 +276,7 @@ private:
VariableLineEdit *_streamTitle;
TwitchCategoryWidget *_category;
TagListWidget *_tags;
ContentClassificationEdit *_contentClassification;
VariableLineEdit *_markerDescription;
QCheckBox *_clipHasDelay;
DurationSelection *_duration;