From 1278ebe7ce4cbdaf89c15c6eae530cb12648092d Mon Sep 17 00:00:00 2001 From: Cloud Date: Fri, 6 Mar 2026 19:03:28 +0900 Subject: [PATCH] UI: Add YouTube broadcast language selection combobox - Replaced the boolean 'Send Language' checkbox with a dynamic 'Language' combobox populated via YouTube's i18nLanguages API. - Automatically sets the default language based on the OBS interface locale (falling back to 'en' if unsupported). - Fixed XML formatting artifacts and QFormLayout layout overlapping issues. - Translated inline Korean comments to English per project guidelines. --- frontend/data/locale/en-US.ini | 1 + frontend/data/locale/ko-KR.ini | 1 + frontend/dialogs/OBSYoutubeActions.cpp | 44 +++++++++++++++++++++- frontend/forms/OBSYoutubeActions.ui | 50 +++++++++++++++++-------- frontend/utility/YoutubeApiWrappers.cpp | 48 ++++++++++++++++++++---- frontend/utility/YoutubeApiWrappers.hpp | 9 ++++- 6 files changed, 128 insertions(+), 25 deletions(-) diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index 882380935c01d6..7e3aba56748459 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -1575,6 +1575,7 @@ YouTube.Actions.EnableAutoStart="Enable Auto-start" YouTube.Actions.EnableAutoStop="Enable Auto-stop" YouTube.Actions.AutoStartStop.TT="Indicates whether this scheduled broadcast should start automatically" YouTube.Actions.EnableDVR="Enable DVR" +YouTube.Actions.SendLanguage="Set broadcast language from OBS language setting" YouTube.Actions.360Video="360 video" YouTube.Actions.360Video.Help="(?)" YouTube.Actions.ScheduleForLater="Schedule for later" diff --git a/frontend/data/locale/ko-KR.ini b/frontend/data/locale/ko-KR.ini index 0fc508fb6602e3..ea090b89bae2b9 100644 --- a/frontend/data/locale/ko-KR.ini +++ b/frontend/data/locale/ko-KR.ini @@ -1250,6 +1250,7 @@ YouTube.Actions.AutoStartStop.TT="예약된 방송이 자동으로 시작될지 YouTube.Actions.EnableDVR="DVR 사용" YouTube.Actions.360Video="360° 동영상" YouTube.Actions.ScheduleForLater="나중을 위해 예약하기" +YouTube.Actions.SendLanguage="OBS 언어 설정을 방송 언어로 자동 설정" YouTube.Actions.RememberSettings="이 설정 기억하기" YouTube.Actions.Create_Ready="방송 생성" YouTube.Actions.Create_GoLive="방송 생성 후 생방송 시작하기" diff --git a/frontend/dialogs/OBSYoutubeActions.cpp b/frontend/dialogs/OBSYoutubeActions.cpp index cf1c1843082a84..1d53ae3f0df94b 100644 --- a/frontend/dialogs/OBSYoutubeActions.cpp +++ b/frontend/dialogs/OBSYoutubeActions.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -146,6 +147,32 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, bool broadcast } } + QVector language_list; + if (apiYouTube->GetI18nLanguagesList(language_list)) { + QString obs_language = QLocale::languageToCode(QLocale(App()->GetLocale()).language()); + bool obs_language_found = false; + + for (auto &language : language_list) { + ui->languageBox->addItem(language.name, language.id); + if (language.id == obs_language) { + obs_language_found = true; + } + } + + if (obs_language_found) { + ui->languageBox->setCurrentIndex(ui->languageBox->findData(obs_language)); + } else { + int en_index = ui->languageBox->findData("en"); + if (en_index != -1) { + ui->languageBox->setCurrentIndex(en_index); + } else if (ui->languageBox->count() > 0) { + ui->languageBox->setCurrentIndex(0); + } + } + } else { + ui->languageBox->setEnabled(false); + } + connect(ui->okButton, &QPushButton::clicked, this, &OBSYoutubeActions::InitBroadcast); connect(ui->saveButton, &QPushButton::clicked, this, &OBSYoutubeActions::ReadyBroadcast); connect(ui->cancelButton, &QPushButton::clicked, this, [&]() { @@ -388,8 +415,8 @@ bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api, BroadcastDesc blog(LOG_DEBUG, "No broadcast created."); return false; } - if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title, broadcast.description, - broadcast.category.id)) { + if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title, broadcast.description, broadcast.category.id, + broadcast.language)) { blog(LOG_DEBUG, "No category set."); return false; } @@ -608,6 +635,9 @@ void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast) broadcast.schedul_for_later = ui->checkScheduledLater->isChecked(); broadcast.projection = ui->check360Video->isChecked() ? "360" : "rectangular"; + // Send selected language to YouTube API + broadcast.language = ui->languageBox->currentData().toString(); + if (ui->checkRememberSettings->isChecked()) SaveSettings(broadcast); } @@ -628,6 +658,7 @@ void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast) config_set_bool(main->activeConfiguration, "YouTube", "ScheduleForLater", broadcast.schedul_for_later); config_set_string(main->activeConfiguration, "YouTube", "Projection", QT_TO_UTF8(broadcast.projection)); config_set_string(main->activeConfiguration, "YouTube", "ThumbnailFile", QT_TO_UTF8(thumbnailFile)); + config_set_string(main->activeConfiguration, "YouTube", "Language", QT_TO_UTF8(broadcast.language)); config_set_bool(main->activeConfiguration, "YouTube", "RememberSettings", true); } @@ -679,6 +710,15 @@ void OBSYoutubeActions::LoadSettings() ui->check360Video->setChecked(false); } + // Load language setting + const char *langStr = config_get_string(main->activeConfiguration, "YouTube", "Language"); + if (langStr && *langStr) { + int langIndex = ui->languageBox->findData(QT_UTF8(langStr)); + if (langIndex != -1) { + ui->languageBox->setCurrentIndex(langIndex); + } + } + const char *thumbFile = config_get_string(main->activeConfiguration, "YouTube", "ThumbnailFile"); if (thumbFile && *thumbFile) { QFileInfo tFile(thumbFile); diff --git a/frontend/forms/OBSYoutubeActions.ui b/frontend/forms/OBSYoutubeActions.ui index 1f38008cda7b2a..6b7e2b442621bd 100644 --- a/frontend/forms/OBSYoutubeActions.ui +++ b/frontend/forms/OBSYoutubeActions.ui @@ -177,13 +177,33 @@ + + + Basic.Settings.General.Language + + + languageBox + + + + + + + + 0 + 0 + + + + + YouTube.Actions.MadeForKids - + @@ -210,21 +230,21 @@ - + YouTube.Actions.MadeForKids.Yes - + YouTube.Actions.Thumbnail - + true @@ -258,7 +278,7 @@ - + 0 @@ -282,7 +302,7 @@ - + Qt::Vertical @@ -295,7 +315,7 @@ - + @@ -307,7 +327,7 @@ - + true @@ -317,7 +337,7 @@ - + @@ -357,7 +377,7 @@ - + @@ -406,7 +426,7 @@ - + true @@ -419,7 +439,7 @@ - + true @@ -448,7 +468,7 @@ - + YouTube.Actions.EnableDVR @@ -458,7 +478,7 @@ - + YouTube.Actions.Latency @@ -468,7 +488,7 @@ - + diff --git a/frontend/utility/YoutubeApiWrappers.cpp b/frontend/utility/YoutubeApiWrappers.cpp index 4d1a2a1ea598f4..3831da7b24af82 100644 --- a/frontend/utility/YoutubeApiWrappers.cpp +++ b/frontend/utility/YoutubeApiWrappers.cpp @@ -24,6 +24,7 @@ constexpr auto youtubeLiveBroadcastBindUrl = "https://www.googleapis.com/youtube constexpr auto youtubeLiveChannelUrl = "https://www.googleapis.com/youtube/v3/channels"sv; constexpr auto youtubeLiveTokenUrl = "https://oauth2.googleapis.com/token"sv; +constexpr auto youtubeLiveI18nLanguagesUrl = "https://www.googleapis.com/youtube/v3/i18nLanguages"sv; constexpr auto youtubeLiveVideoCategoriesUrl = "https://www.googleapis.com/youtube/v3/videoCategories"sv; constexpr auto youtubeLiveVideosUrl = "https://www.googleapis.com/youtube/v3/videos"sv; constexpr auto youtubeLiveThumbnailUrl = "https://www.googleapis.com/upload/youtube/v3/thumbnails/set"sv; @@ -275,6 +276,31 @@ bool YoutubeApiWrappers::GetBroadcastsList(Json &json_out, const QString &page, return InsertCommand(url.c_str(), "application/json", "", nullptr, json_out); } +bool YoutubeApiWrappers::GetI18nLanguagesList(QVector &language_list_out) +{ + lastErrorMessage.clear(); + lastErrorReason.clear(); + const QString url_template = QString(youtubeLiveI18nLanguagesUrl.data()) + "?part=snippet&hl=%1"; + + QString url = url_template.arg(QLocale().name()); + + Json json_out; + if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr, json_out)) { + if (lastErrorReason != "unsupportedLanguageCode" && lastErrorReason != "invalidLanguage") + return false; + // Try again with en_US if YouTube error indicates an unsupported locale + url = url_template.arg("en_US"); + if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr, json_out)) + return false; + } + language_list_out = {}; + for (auto &j : json_out["items"].array_items()) { + language_list_out.push_back( + {j["id"].string_value().c_str(), j["snippet"]["name"].string_value().c_str()}); + } + return language_list_out.isEmpty() ? false : true; +} + bool YoutubeApiWrappers::GetVideoCategoriesList(QVector &category_list_out) { lastErrorMessage.clear(); @@ -315,19 +341,27 @@ bool YoutubeApiWrappers::GetVideoCategoriesList(QVector &ca } bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id, const QString &video_title, - const QString &video_description, const QString &categorie_id) + const QString &video_description, const QString &categorie_id, + const QString &language) { lastErrorMessage.clear(); lastErrorReason.clear(); const std::string url = std::string(youtubeLiveVideosUrl) + "?part=snippet"; + + Json::object snippet = { + {"title", QT_TO_UTF8(video_title)}, + {"description", QT_TO_UTF8(video_description)}, + {"categoryId", QT_TO_UTF8(categorie_id)}, + }; + + if (!language.isEmpty()) { + snippet["defaultLanguage"] = QT_TO_UTF8(language); + snippet["defaultAudioLanguage"] = QT_TO_UTF8(language); + } + const Json data = Json::object{ {"id", QT_TO_UTF8(video_id)}, - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(video_title)}, - {"description", QT_TO_UTF8(video_description)}, - {"categoryId", QT_TO_UTF8(categorie_id)}, - }}, + {"snippet", snippet}, }; Json json_out; return InsertCommand(url.c_str(), "application/json", "PUT", data.dump().c_str(), json_out); diff --git a/frontend/utility/YoutubeApiWrappers.hpp b/frontend/utility/YoutubeApiWrappers.hpp index 9cabb709138756..d2bb6ec06668cd 100644 --- a/frontend/utility/YoutubeApiWrappers.hpp +++ b/frontend/utility/YoutubeApiWrappers.hpp @@ -22,6 +22,11 @@ struct CategoryDescription { QString title; }; +struct I18nLanguageDescription { + QString id; + QString name; +}; + struct BroadcastDescription { QString id; QString title; @@ -36,6 +41,7 @@ struct BroadcastDescription { bool schedul_for_later; QString schedul_date_time; QString projection; + QString language; }; bool IsYouTubeService(const std::string &service); @@ -59,8 +65,9 @@ class YoutubeApiWrappers : public YoutubeAuth { bool BindStream(const QString broadcast_id, const QString stream_id); bool GetBroadcastsList(json11::Json &json_out, const QString &page, const QString &status); bool GetVideoCategoriesList(QVector &category_list_out); + bool GetI18nLanguagesList(QVector &language_list_out); bool SetVideoCategory(const QString &video_id, const QString &video_title, const QString &video_description, - const QString &categorie_id); + const QString &categorie_id, const QString &language = ""); bool SetVideoThumbnail(const QString &video_id, const QString &thumbnail_file); bool StartBroadcast(const QString &broadcast_id); bool StopBroadcast(const QString &broadcast_id);