mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-03-22 01:44:49 -05:00
* No longer provide multiple HTTP helper functions with the same name * Default to cache not being used * Explicitly use the cache for certain Twitch actions and conditions * Clear caches on shutdown to avoid leaks being reported
395 lines
10 KiB
C++
395 lines
10 KiB
C++
#include "category-selection.hpp"
|
|
#include "token.hpp"
|
|
#include "twitch-helpers.hpp"
|
|
|
|
#include <name-dialog.hpp>
|
|
#include <obs-module-helper.hpp>
|
|
#include <obs.hpp>
|
|
#include <path-helpers.hpp>
|
|
#include <QVBoxLayout>
|
|
#include <ui-helpers.hpp>
|
|
#include <utility.hpp>
|
|
|
|
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<QString, int> 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<TwitchToken> &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<QString, int> CategoryGrabber::_categoryMap = {};
|
|
std::mutex CategoryGrabber::_mtx = {};
|
|
|
|
void CategoryGrabber::Start(const std::shared_ptr<TwitchToken> &token,
|
|
const std::string search)
|
|
{
|
|
_searchString = search;
|
|
_token = token;
|
|
_stop = false;
|
|
start();
|
|
}
|
|
|
|
void CategoryGrabber::Stop()
|
|
{
|
|
_stop = true;
|
|
}
|
|
|
|
const std::map<QString, int> &CategoryGrabber::GetCategories()
|
|
{
|
|
return _categoryMap;
|
|
}
|
|
|
|
void CategoryGrabber::run()
|
|
{
|
|
if (!_token) {
|
|
emit Failed();
|
|
return;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> 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<TwitchToken> &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<TwitchToken> &token)
|
|
{
|
|
_selection->SetToken(token);
|
|
_manualSearch->SetToken(token);
|
|
}
|
|
|
|
TwitchCategorySignalManager *TwitchCategorySignalManager::Instance()
|
|
{
|
|
static TwitchCategorySignalManager manager;
|
|
return &manager;
|
|
}
|
|
|
|
} // namespace advss
|