#include "category-selection.hpp" #include "token.hpp" #include "twitch-helpers.hpp" #include #include #include #include #include #include #include namespace advss { void TwitchCategory::Load(obs_data_t *obj) { OBSDataAutoRelease data = obs_data_get_obj(obj, "category"); id = obs_data_get_int(data, "id"); name = obs_data_get_string(data, "name"); } void TwitchCategory::Save(obs_data_t *obj) const { OBSDataAutoRelease data = obs_data_create(); obs_data_set_int(data, "id", id); obs_data_set_string(data, "name", name.c_str()); obs_data_set_obj(obj, "category", data); } bool TwitchCategorySelection::_fetchingCategoriesDone = false; std::map TwitchCategorySelection::_streamingCategories; void TwitchCategorySelection::PopulateCategorySelection() { auto token = _token.lock(); if (!token && _streamingCategories.empty()) { return; } if (!_fetchingCategoriesDone && token) { _categoryGrabber.Start(token); if (_progressDialog->exec() == QDialog::Accepted) { _fetchingCategoriesDone = true; } _categoryGrabber.Stop(); _categoryGrabber.wait(); } UpdateCategoryList(); } void TwitchCategorySelection::UpdateCategoryList() { _streamingCategories = _categoryGrabber.GetCategories(); QString currentSelection = currentText(); const QSignalBlocker b(this); clear(); for (const auto &[name, id] : _streamingCategories) { addItem(name, id); } setCurrentText(currentSelection); } TwitchCategorySelection::TwitchCategorySelection(QWidget *parent) : FilterComboBox( parent, obs_module_text("AdvSceneSwitcher.twitchCategories.select")), _progressDialog(new ProgressDialog(this)) { _progressDialog->setWindowModality(Qt::WindowModal); setSizeAdjustPolicy(QComboBox::AdjustToContents); QWidget::connect(this, SIGNAL(currentIndexChanged(int)), this, SLOT(SelectionChanged(int))); QWidget::connect(&_categoryGrabber, SIGNAL(CategoryCountUpdated(int)), _progressDialog, SLOT(CategoryCountUpdated(int))); QWidget::connect(&_categoryGrabber, SIGNAL(Finished()), this, SLOT(PopulateFinished())); QWidget::connect(TwitchCategorySignalManager::Instance(), SIGNAL(RepopulateRequired()), this, SLOT(UpdateCategoryList())); } void TwitchCategorySelection::SetToken(const std::weak_ptr &token) { _token = token; const bool expired = token.expired(); setDisabled(expired); if (expired) { setToolTip(obs_module_text( "AdvSceneSwitcher.action.twitch.categorySelectionDisabled")); } else { setToolTip(""); } } void TwitchCategorySelection::PopulateFinished() { _progressDialog->accept(); } void TwitchCategorySelection::showPopup() { if (!IsPopulated()) { PopulateCategorySelection(); } adjustSize(); updateGeometry(); FilterComboBox::showPopup(); } bool TwitchCategorySelection::IsPopulated() { return count() == (int)_categoryGrabber.GetCategories().size() && _fetchingCategoriesDone; } void TwitchCategorySelection::SetCategory(const TwitchCategory &id) { // If the list is populated already try to find id ... int index = findData(id.id); if (index != -1) { setCurrentIndex(index); return; } if (id.id == -1) { setCurrentIndex(-1); return; } // ... otherwise just add a dummy entry with the category name addItem(QString::fromStdString(id.name), id.id); setCurrentIndex(findData(id.id)); } void TwitchCategorySelection::SelectionChanged(int index) { TwitchCategory category{itemData(index).toInt(), currentText().toStdString()}; emit CategoryChanged(category); } std::map CategoryGrabber::_categoryMap = {}; std::mutex CategoryGrabber::_mtx = {}; void CategoryGrabber::Start(const std::shared_ptr &token, const std::string search) { _searchString = search; _token = token; _stop = false; start(); } void CategoryGrabber::Stop() { _stop = true; } const std::map &CategoryGrabber::GetCategories() { return _categoryMap; } void CategoryGrabber::run() { if (!_token) { emit Failed(); return; } { std::lock_guard lock(_mtx); if (_searchString.empty()) { GetAll(); } else { Search(_searchString); } } emit Finished(); } void CategoryGrabber::Search(const std::string &) { static const std::string uri = "https://api.twitch.tv"; const std::string path = "/helix/search/categories"; int startCount = _categoryMap.size(); std::string cursor; httplib::Params params = {{"first", "100"}, {"after", cursor}, {"query", _searchString}}; auto response = SendGetRequest(*_token, uri, path, params, true); while (response.status == 200 && !_stop) { cursor = ParseReply(response.data); if (cursor.empty()) { break; // End of category list } params = {{"first", "100"}, {"after", cursor}, {"query", _searchString}}; response = SendGetRequest(*_token, uri, path, params, true); emit CategoryCountUpdated(_categoryMap.size() - startCount); } } void CategoryGrabber::GetAll() { static const std::string uri = "https://api.twitch.tv"; const std::string path = "/helix/games/top"; // Declare static to "save" progress in case of cancel static std::string cursor; httplib::Params params = {{"first", "100"}, {"after", cursor}}; auto response = SendGetRequest(*_token, uri, path, params, true); while (response.status == 200 && !_stop) { cursor = ParseReply(response.data); if (cursor.empty()) { break; // End of category list } params = {{"first", "100"}, {"after", cursor}}; response = SendGetRequest(*_token, uri, path, params, true); emit CategoryCountUpdated(_categoryMap.size()); } } std::string CategoryGrabber::ParseReply(obs_data_t *data) const { OBSDataArrayAutoRelease array = obs_data_get_array(data, "data"); size_t count = obs_data_array_count(array); for (size_t i = 0; i < count; i++) { OBSDataAutoRelease arrayObj = obs_data_array_item(array, i); int id = std::stoi(obs_data_get_string(arrayObj, "id")); QString name = obs_data_get_string(arrayObj, "name"); _categoryMap.emplace(name, id); } OBSDataAutoRelease pagination = obs_data_get_obj(data, "pagination"); return obs_data_get_string(pagination, "cursor"); } ProgressDialog::ProgressDialog(QWidget *parent, bool showSkip) : QDialog(parent), _skipFetchCheckBox(new QCheckBox(obs_module_text( "AdvSceneSwitcher.twitchCategories.fetchSkip"))), _status(new QLabel(obs_module_text( "AdvSceneSwitcher.twitchCategories.fetchStart"))) { setWindowTitle(obs_module_text("AdvSceneSwitcher.windowTitle")); auto layout = new QVBoxLayout(this); layout->addWidget(_status); auto cancelButton = new QPushButton( obs_module_text("AdvSceneSwitcher.twitchCategories.fetchStop"), this); layout->addWidget(_skipFetchCheckBox); layout->addWidget(cancelButton); setLayout(layout); QWidget::connect(_skipFetchCheckBox, &QCheckBox::stateChanged, this, [this](int value) { _skipFetch = value; }); QWidget::connect(cancelButton, &QPushButton::clicked, this, [this]() { _skipFetch ? accept() : reject(); }); _skipFetchCheckBox->setVisible(showSkip); if (_skipFetch) { accept(); } } void ProgressDialog::CategoryCountUpdated(int value) { _status->setText( QString(obs_module_text( "AdvSceneSwitcher.twitchCategories.fetchStatus")) .arg(value)); } TwitchCategorySearchButton::TwitchCategorySearchButton(QWidget *parent) : QToolButton(parent) { const std::string pathPrefix = GetDataFilePath("res/images/" + GetThemeTypeName()); SetButtonIcon(this, (pathPrefix + "Search.svg").c_str()); setToolTip(obs_module_text( "AdvSceneSwitcher.twitchCategories.manualSearch")); QWidget::connect(this, SIGNAL(clicked()), this, SLOT(StartManualCategorySearch())); QWidget::connect(this, SIGNAL(RequestRepopulate()), TwitchCategorySignalManager::Instance(), SIGNAL(RepopulateRequired())); } void TwitchCategorySearchButton::SetToken( const std::weak_ptr &token) { _token = token; const bool expired = token.expired(); setDisabled(expired); if (expired) { setToolTip(obs_module_text( "AdvSceneSwitcher.action.twitch.categorySelectionDisabled")); } else { setToolTip(obs_module_text( "AdvSceneSwitcher.twitchCategories.manualSearch")); } } void TwitchCategorySearchButton::StartManualCategorySearch() { std::string category; bool accepted = NameDialog::AskForName( this, obs_module_text("AdvSceneSwitcher.twitchCategories.search"), obs_module_text("AdvSceneSwitcher.twitchCategories.name"), category); if (!accepted) { return; } CategoryGrabber categoryGrabber; auto *progressDialog = new ProgressDialog(this, false); QWidget::connect(&categoryGrabber, SIGNAL(CategoryCountUpdated(int)), progressDialog, SLOT(CategoryCountUpdated(int))); QWidget::connect(&categoryGrabber, &CategoryGrabber::Finished, this, [progressDialog]() { progressDialog->accept(); }); QWidget::connect(&categoryGrabber, &CategoryGrabber::Failed, this, [progressDialog]() { progressDialog->reject(); }); auto previousCategoryCount = categoryGrabber.GetCategories().size(); categoryGrabber.Start(_token.lock(), category); progressDialog->exec(); categoryGrabber.Stop(); categoryGrabber.wait(); emit RequestRepopulate(); progressDialog->deleteLater(); auto newCategoryCount = categoryGrabber.GetCategories().size() - previousCategoryCount; if (newCategoryCount == 0) { DisplayMessage( QString(obs_module_text( "AdvSceneSwitcher.twitchCategories.searchFailed")) .arg(QString::fromStdString(category))); } else { DisplayMessage( QString(obs_module_text( "AdvSceneSwitcher.twitchCategories.searchSuccess")) .arg(QString::number(newCategoryCount), QString::fromStdString(category))); } } TwitchCategoryWidget::TwitchCategoryWidget(QWidget *parent) : QWidget(parent), _selection(new TwitchCategorySelection(this)), _manualSearch(new TwitchCategorySearchButton(this)) { QWidget::connect(_selection, SIGNAL(CategoryChanged(const TwitchCategory &)), this, SIGNAL(CategoryChanged(const TwitchCategory &))); auto layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(_selection); layout->addWidget(_manualSearch); setLayout(layout); } void TwitchCategoryWidget::SetCategory(const TwitchCategory &category) { _selection->SetCategory(category); } void TwitchCategoryWidget::SetToken(const std::weak_ptr &token) { _selection->SetToken(token); _manualSearch->SetToken(token); } TwitchCategorySignalManager *TwitchCategorySignalManager::Instance() { static TwitchCategorySignalManager manager; return &manager; } } // namespace advss