window-youtube-actions.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. #include "window-basic-main.hpp"
  2. #include "window-youtube-actions.hpp"
  3. #include "obs-app.hpp"
  4. #include "qt-wrappers.hpp"
  5. #include "youtube-api-wrappers.hpp"
  6. #include <QToolTip>
  7. #include <QDateTime>
  8. #include <QDesktopServices>
  9. const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'";
  10. const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m";
  11. const QString IndexOfGamingCategory = "20";
  12. OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth)
  13. : QDialog(parent),
  14. ui(new Ui::OBSYoutubeActions),
  15. apiYouTube(dynamic_cast<YoutubeApiWrappers *>(auth)),
  16. workerThread(new WorkerThread(apiYouTube))
  17. {
  18. setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
  19. ui->setupUi(this);
  20. ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Public"),
  21. "public");
  22. ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Unlisted"),
  23. "unlisted");
  24. ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Private"),
  25. "private");
  26. ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Normal"),
  27. "normal");
  28. ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Low"), "low");
  29. ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.UltraLow"),
  30. "ultraLow");
  31. UpdateOkButtonStatus();
  32. connect(ui->title, &QLineEdit::textChanged, this,
  33. [&](const QString &) { this->UpdateOkButtonStatus(); });
  34. connect(ui->privacyBox, &QComboBox::currentTextChanged, this,
  35. [&](const QString &) { this->UpdateOkButtonStatus(); });
  36. connect(ui->yesMakeForKids, &QRadioButton::toggled, this,
  37. [&](bool) { this->UpdateOkButtonStatus(); });
  38. connect(ui->notMakeForKids, &QRadioButton::toggled, this,
  39. [&](bool) { this->UpdateOkButtonStatus(); });
  40. connect(ui->tabWidget, &QTabWidget::currentChanged, this,
  41. [&](int) { this->UpdateOkButtonStatus(); });
  42. connect(ui->pushButton, &QPushButton::clicked, this,
  43. &OBSYoutubeActions::OpenYouTubeDashboard);
  44. connect(ui->helpAutoStartStop, &QLabel::linkActivated, this,
  45. [](const QString &) {
  46. QToolTip::showText(
  47. QCursor::pos(),
  48. QTStr("YouTube.Actions.AutoStartStop.TT"));
  49. });
  50. connect(ui->help360Video, &QLabel::linkActivated, this,
  51. [](const QString &link) { QDesktopServices::openUrl(link); });
  52. connect(ui->helpMadeForKids, &QLabel::linkActivated, this,
  53. [](const QString &link) { QDesktopServices::openUrl(link); });
  54. ui->scheduledTime->setVisible(false);
  55. connect(ui->checkScheduledLater, &QCheckBox::stateChanged, this,
  56. [&](int state) {
  57. ui->scheduledTime->setVisible(state);
  58. if (state) {
  59. ui->checkAutoStart->setVisible(true);
  60. ui->checkAutoStop->setVisible(true);
  61. ui->helpAutoStartStop->setVisible(true);
  62. ui->checkAutoStart->setChecked(false);
  63. ui->checkAutoStop->setChecked(false);
  64. } else {
  65. ui->checkAutoStart->setVisible(false);
  66. ui->checkAutoStop->setVisible(false);
  67. ui->helpAutoStartStop->setVisible(false);
  68. ui->checkAutoStart->setChecked(true);
  69. ui->checkAutoStop->setChecked(true);
  70. }
  71. UpdateOkButtonStatus();
  72. });
  73. ui->checkAutoStart->setVisible(false);
  74. ui->checkAutoStop->setVisible(false);
  75. ui->helpAutoStartStop->setVisible(false);
  76. ui->scheduledTime->setDateTime(QDateTime::currentDateTime());
  77. if (!apiYouTube) {
  78. blog(LOG_DEBUG, "YouTube API auth NOT found.");
  79. Cancel();
  80. return;
  81. }
  82. ChannelDescription channel;
  83. if (!apiYouTube->GetChannelDescription(channel)) {
  84. blog(LOG_DEBUG, "Could not get channel description.");
  85. ShowErrorDialog(
  86. parent,
  87. apiYouTube->GetLastError().isEmpty()
  88. ? QTStr("YouTube.Actions.Error.General")
  89. : QTStr("YouTube.Actions.Error.Text")
  90. .arg(apiYouTube->GetLastError()));
  91. Cancel();
  92. return;
  93. }
  94. this->setWindowTitle(channel.title);
  95. QVector<CategoryDescription> category_list;
  96. if (!apiYouTube->GetVideoCategoriesList(category_list)) {
  97. ShowErrorDialog(
  98. parent,
  99. apiYouTube->GetLastError().isEmpty()
  100. ? QTStr("YouTube.Actions.Error.General")
  101. : QTStr("YouTube.Actions.Error.Text")
  102. .arg(apiYouTube->GetLastError()));
  103. Cancel();
  104. return;
  105. }
  106. for (auto &category : category_list) {
  107. ui->categoryBox->addItem(category.title, category.id);
  108. if (category.id == IndexOfGamingCategory) {
  109. ui->categoryBox->setCurrentText(category.title);
  110. }
  111. }
  112. connect(ui->okButton, &QPushButton::clicked, this,
  113. &OBSYoutubeActions::InitBroadcast);
  114. connect(ui->cancelButton, &QPushButton::clicked, this, [&]() {
  115. blog(LOG_DEBUG, "YouTube live broadcast creation cancelled.");
  116. // Close the dialog.
  117. Cancel();
  118. });
  119. qDeleteAll(ui->scrollAreaWidgetContents->findChildren<QWidget *>(
  120. QString(), Qt::FindDirectChildrenOnly));
  121. // Add label indicating loading state
  122. QLabel *loadingLabel = new QLabel();
  123. loadingLabel->setTextFormat(Qt::RichText);
  124. loadingLabel->setAlignment(Qt::AlignHCenter);
  125. loadingLabel->setText(
  126. QString("<big>%1</big>")
  127. .arg(QTStr("YouTube.Actions.EventsLoading")));
  128. ui->scrollAreaWidgetContents->layout()->addWidget(loadingLabel);
  129. // Delete "loading..." label on completion
  130. connect(workerThread, &WorkerThread::finished, this, [&] {
  131. QLayoutItem *item =
  132. ui->scrollAreaWidgetContents->layout()->takeAt(0);
  133. item->widget()->deleteLater();
  134. });
  135. connect(workerThread, &WorkerThread::failed, this, [&]() {
  136. auto last_error = apiYouTube->GetLastError();
  137. if (last_error.isEmpty())
  138. last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
  139. if (!apiYouTube->GetTranslatedError(last_error))
  140. last_error = QTStr("YouTube.Actions.Error.Text")
  141. .arg(last_error);
  142. ShowErrorDialog(this, last_error);
  143. QDialog::reject();
  144. });
  145. connect(workerThread, &WorkerThread::new_item, this,
  146. [&](const QString &title, const QString &dateTimeString,
  147. const QString &broadcast, const QString &status,
  148. bool astart, bool astop) {
  149. ClickableLabel *label = new ClickableLabel();
  150. label->setTextFormat(Qt::RichText);
  151. if (status == "live" || status == "testing") {
  152. // Resumable stream
  153. label->setText(
  154. QString("<big>%1</big><br/>%2")
  155. .arg(title,
  156. QTStr("YouTube.Actions.Stream.Resume")));
  157. } else if (dateTimeString.isEmpty()) {
  158. // The broadcast created by YouTube Studio has no start time.
  159. // Yes this does violate the restrictions set in YouTube's API
  160. // But why would YouTube care about consistency?
  161. label->setText(
  162. QString("<big>%1</big><br/>%2")
  163. .arg(title,
  164. QTStr("YouTube.Actions.Stream.YTStudio")));
  165. } else {
  166. label->setText(
  167. QString("<big>%1</big><br/>%2")
  168. .arg(title,
  169. QTStr("YouTube.Actions.Stream.ScheduledFor")
  170. .arg(dateTimeString)));
  171. }
  172. label->setAlignment(Qt::AlignHCenter);
  173. label->setMargin(4);
  174. connect(label, &ClickableLabel::clicked, this,
  175. [&, label, broadcast, astart, astop]() {
  176. for (QWidget *i :
  177. ui->scrollAreaWidgetContents->findChildren<
  178. QWidget *>(
  179. QString(),
  180. Qt::FindDirectChildrenOnly)) {
  181. i->setProperty(
  182. "isSelectedEvent",
  183. "false");
  184. i->style()->unpolish(i);
  185. i->style()->polish(i);
  186. }
  187. label->setProperty("isSelectedEvent",
  188. "true");
  189. label->style()->unpolish(label);
  190. label->style()->polish(label);
  191. this->selectedBroadcast = broadcast;
  192. this->autostart = astart;
  193. this->autostop = astop;
  194. UpdateOkButtonStatus();
  195. });
  196. ui->scrollAreaWidgetContents->layout()->addWidget(
  197. label);
  198. });
  199. workerThread->start();
  200. #ifdef __APPLE__
  201. // MacOS theming issues
  202. this->resize(this->width() + 200, this->height() + 120);
  203. #endif
  204. valid = true;
  205. }
  206. OBSYoutubeActions::~OBSYoutubeActions()
  207. {
  208. workerThread->stop();
  209. workerThread->wait();
  210. delete workerThread;
  211. }
  212. void WorkerThread::run()
  213. {
  214. if (!pending)
  215. return;
  216. json11::Json broadcasts;
  217. for (QString broacastStatus : {"active", "upcoming"}) {
  218. if (!apiYouTube->GetBroadcastsList(broadcasts, "",
  219. broacastStatus)) {
  220. emit failed();
  221. return;
  222. }
  223. while (pending) {
  224. auto items = broadcasts["items"].array_items();
  225. for (auto item : items) {
  226. QString status = QString::fromStdString(
  227. item["status"]["lifeCycleStatus"]
  228. .string_value());
  229. if (status == "live" || status == "testing") {
  230. // Check that the attached liveStream is offline (reconnectable)
  231. QString stream_id = QString::fromStdString(
  232. item["contentDetails"]
  233. ["boundStreamId"]
  234. .string_value());
  235. json11::Json stream;
  236. if (!apiYouTube->FindStream(stream_id,
  237. stream))
  238. continue;
  239. if (stream["status"]["streamStatus"] ==
  240. "active")
  241. continue;
  242. }
  243. QString title = QString::fromStdString(
  244. item["snippet"]["title"].string_value());
  245. QString scheduledStartTime =
  246. QString::fromStdString(
  247. item["snippet"]
  248. ["scheduledStartTime"]
  249. .string_value());
  250. QString broadcast = QString::fromStdString(
  251. item["id"].string_value());
  252. // Treat already started streams as autostart for UI purposes
  253. bool astart =
  254. status == "live"
  255. ? true
  256. : item["contentDetails"]
  257. ["enableAutoStart"]
  258. .bool_value();
  259. bool astop =
  260. item["contentDetails"]["enableAutoStop"]
  261. .bool_value();
  262. QDateTime utcDTime = QDateTime::fromString(
  263. scheduledStartTime,
  264. SchedulDateAndTimeFormat);
  265. // DateTime parser means that input datetime is a local, so we need to move it
  266. QDateTime dateTime = utcDTime.addSecs(
  267. utcDTime.offsetFromUtc());
  268. QString dateTimeString = QLocale().toString(
  269. dateTime,
  270. QString("%1 %2").arg(
  271. QLocale().dateFormat(
  272. QLocale::LongFormat),
  273. QLocale().timeFormat(
  274. QLocale::ShortFormat)));
  275. emit new_item(title, dateTimeString, broadcast,
  276. status, astart, astop);
  277. }
  278. auto nextPageToken =
  279. broadcasts["nextPageToken"].string_value();
  280. if (nextPageToken.empty() || items.empty())
  281. break;
  282. else {
  283. if (!pending)
  284. return;
  285. if (!apiYouTube->GetBroadcastsList(
  286. broadcasts,
  287. QString::fromStdString(
  288. nextPageToken),
  289. broacastStatus)) {
  290. emit failed();
  291. return;
  292. }
  293. }
  294. }
  295. }
  296. emit ready();
  297. }
  298. void OBSYoutubeActions::UpdateOkButtonStatus()
  299. {
  300. if (ui->tabWidget->currentIndex() == 0) {
  301. ui->okButton->setEnabled(
  302. !ui->title->text().isEmpty() &&
  303. !ui->privacyBox->currentText().isEmpty() &&
  304. (ui->yesMakeForKids->isChecked() ||
  305. ui->notMakeForKids->isChecked()));
  306. if (ui->checkScheduledLater->checkState() == Qt::Checked) {
  307. ui->okButton->setText(
  308. QTStr("YouTube.Actions.Create_Save"));
  309. } else {
  310. ui->okButton->setText(
  311. QTStr("YouTube.Actions.Create_GoLive"));
  312. }
  313. ui->pushButton->setVisible(false);
  314. } else {
  315. ui->okButton->setEnabled(!selectedBroadcast.isEmpty());
  316. ui->okButton->setText(QTStr("YouTube.Actions.Choose_GoLive"));
  317. ui->pushButton->setVisible(true);
  318. }
  319. }
  320. bool OBSYoutubeActions::StreamNowAction(YoutubeApiWrappers *api,
  321. StreamDescription &stream)
  322. {
  323. YoutubeApiWrappers *apiYouTube = api;
  324. BroadcastDescription broadcast = {};
  325. UiToBroadcast(broadcast);
  326. // stream now is always autostart/autostop
  327. broadcast.auto_start = true;
  328. broadcast.auto_stop = true;
  329. blog(LOG_DEBUG, "Scheduled date and time: %s",
  330. broadcast.schedul_date_time.toStdString().c_str());
  331. if (!apiYouTube->InsertBroadcast(broadcast)) {
  332. blog(LOG_DEBUG, "No broadcast created.");
  333. return false;
  334. }
  335. stream = {"", "", "OBS Studio Video Stream"};
  336. if (!apiYouTube->InsertStream(stream)) {
  337. blog(LOG_DEBUG, "No stream created.");
  338. return false;
  339. }
  340. if (!apiYouTube->BindStream(broadcast.id, stream.id)) {
  341. blog(LOG_DEBUG, "No stream binded.");
  342. return false;
  343. }
  344. if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title,
  345. broadcast.description,
  346. broadcast.category.id)) {
  347. blog(LOG_DEBUG, "No category set.");
  348. return false;
  349. }
  350. if (broadcast.privacy != "private")
  351. apiYouTube->SetChatId(broadcast.id);
  352. else
  353. apiYouTube->ResetChat();
  354. return true;
  355. }
  356. bool OBSYoutubeActions::StreamLaterAction(YoutubeApiWrappers *api)
  357. {
  358. YoutubeApiWrappers *apiYouTube = api;
  359. BroadcastDescription broadcast = {};
  360. UiToBroadcast(broadcast);
  361. // DateTime parser means that input datetime is a local, so we need to move it
  362. auto dateTime = ui->scheduledTime->dateTime();
  363. auto utcDTime = dateTime.addSecs(-dateTime.offsetFromUtc());
  364. broadcast.schedul_date_time =
  365. utcDTime.toString(SchedulDateAndTimeFormat);
  366. blog(LOG_DEBUG, "Scheduled date and time: %s",
  367. broadcast.schedul_date_time.toStdString().c_str());
  368. if (!apiYouTube->InsertBroadcast(broadcast)) {
  369. blog(LOG_DEBUG, "No broadcast created.");
  370. return false;
  371. }
  372. if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title,
  373. broadcast.description,
  374. broadcast.category.id)) {
  375. blog(LOG_DEBUG, "No category set.");
  376. return false;
  377. }
  378. if (broadcast.privacy != "private")
  379. apiYouTube->SetChatId(broadcast.id);
  380. else
  381. apiYouTube->ResetChat();
  382. return true;
  383. }
  384. bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api,
  385. StreamDescription &stream)
  386. {
  387. YoutubeApiWrappers *apiYouTube = api;
  388. json11::Json json;
  389. if (!apiYouTube->FindBroadcast(selectedBroadcast, json)) {
  390. blog(LOG_DEBUG, "No broadcast found.");
  391. return false;
  392. }
  393. std::string boundStreamId =
  394. json["items"]
  395. .array_items()[0]["contentDetails"]["boundStreamId"]
  396. .string_value();
  397. std::string broadcastPrivacy =
  398. json["status"]["privacyStatus"].string_value();
  399. stream.id = boundStreamId.c_str();
  400. if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) {
  401. auto item = json["items"].array_items()[0];
  402. auto streamName = item["cdn"]["ingestionInfo"]["streamName"]
  403. .string_value();
  404. auto title = item["snippet"]["title"].string_value();
  405. stream.name = streamName.c_str();
  406. stream.title = title.c_str();
  407. api->SetBroadcastId(selectedBroadcast);
  408. } else {
  409. stream = {"", "", "OBS Studio Video Stream"};
  410. if (!apiYouTube->InsertStream(stream)) {
  411. blog(LOG_DEBUG, "No stream created.");
  412. return false;
  413. }
  414. if (!apiYouTube->BindStream(selectedBroadcast, stream.id)) {
  415. blog(LOG_DEBUG, "No stream binded.");
  416. return false;
  417. }
  418. }
  419. if (broadcastPrivacy != "private")
  420. apiYouTube->SetChatId(selectedBroadcast);
  421. else
  422. apiYouTube->ResetChat();
  423. return true;
  424. }
  425. void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text)
  426. {
  427. QMessageBox dlg(parent);
  428. dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint);
  429. dlg.setWindowTitle(QTStr("YouTube.Actions.Error.Title"));
  430. dlg.setText(text);
  431. dlg.setTextFormat(Qt::RichText);
  432. dlg.setIcon(QMessageBox::Warning);
  433. dlg.setStandardButtons(QMessageBox::StandardButton::Ok);
  434. dlg.exec();
  435. }
  436. void OBSYoutubeActions::InitBroadcast()
  437. {
  438. StreamDescription stream;
  439. QMessageBox msgBox(this);
  440. msgBox.setWindowFlags(msgBox.windowFlags() &
  441. ~Qt::WindowCloseButtonHint);
  442. msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title"));
  443. msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast"));
  444. msgBox.setStandardButtons(QMessageBox::StandardButtons());
  445. bool success = false;
  446. auto action = [&]() {
  447. if (ui->tabWidget->currentIndex() == 0) {
  448. if (ui->checkScheduledLater->isChecked()) {
  449. success = this->StreamLaterAction(apiYouTube);
  450. } else {
  451. success = this->StreamNowAction(apiYouTube,
  452. stream);
  453. }
  454. } else {
  455. success = this->ChooseAnEventAction(apiYouTube, stream);
  456. };
  457. QMetaObject::invokeMethod(&msgBox, "accept",
  458. Qt::QueuedConnection);
  459. };
  460. QScopedPointer<QThread> thread(CreateQThread(action));
  461. thread->start();
  462. msgBox.exec();
  463. thread->wait();
  464. if (success) {
  465. if (ui->tabWidget->currentIndex() == 0) {
  466. // Stream later usecase.
  467. if (ui->checkScheduledLater->isChecked()) {
  468. QMessageBox msg(this);
  469. msg.setWindowTitle(QTStr(
  470. "YouTube.Actions.EventCreated.Title"));
  471. msg.setText(QTStr(
  472. "YouTube.Actions.EventCreated.Text"));
  473. msg.setStandardButtons(QMessageBox::Ok);
  474. msg.exec();
  475. // Close dialog without start streaming.
  476. Cancel();
  477. } else {
  478. // Stream now usecase.
  479. blog(LOG_DEBUG, "New valid stream: %s",
  480. QT_TO_UTF8(stream.name));
  481. emit ok(QT_TO_UTF8(stream.id),
  482. QT_TO_UTF8(stream.name), true, true);
  483. Accept();
  484. }
  485. } else {
  486. // Stream to precreated broadcast usecase.
  487. emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name),
  488. autostart, autostop);
  489. Accept();
  490. }
  491. } else {
  492. // Fail.
  493. auto last_error = apiYouTube->GetLastError();
  494. if (last_error.isEmpty())
  495. last_error = QTStr("YouTube.Actions.Error.YouTubeApi");
  496. if (!apiYouTube->GetTranslatedError(last_error))
  497. last_error =
  498. QTStr("YouTube.Actions.Error.NoBroadcastCreated")
  499. .arg(last_error);
  500. ShowErrorDialog(this, last_error);
  501. }
  502. }
  503. void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast)
  504. {
  505. broadcast.title = ui->title->text();
  506. // ToDo: UI warning rather than silent truncation
  507. broadcast.description = ui->description->toPlainText().left(5000);
  508. broadcast.privacy = ui->privacyBox->currentData().toString();
  509. broadcast.category.title = ui->categoryBox->currentText();
  510. broadcast.category.id = ui->categoryBox->currentData().toString();
  511. broadcast.made_for_kids = ui->yesMakeForKids->isChecked();
  512. broadcast.latency = ui->latencyBox->currentData().toString();
  513. broadcast.auto_start = ui->checkAutoStart->isChecked();
  514. broadcast.auto_stop = ui->checkAutoStop->isChecked();
  515. broadcast.dvr = ui->checkDVR->isChecked();
  516. broadcast.schedul_for_later = ui->checkScheduledLater->isChecked();
  517. broadcast.projection = ui->check360Video->isChecked() ? "360"
  518. : "rectangular";
  519. // Current time by default.
  520. broadcast.schedul_date_time = QDateTime::currentDateTimeUtc().toString(
  521. SchedulDateAndTimeFormat);
  522. }
  523. void OBSYoutubeActions::OpenYouTubeDashboard()
  524. {
  525. ChannelDescription channel;
  526. if (!apiYouTube->GetChannelDescription(channel)) {
  527. blog(LOG_DEBUG, "Could not get channel description.");
  528. ShowErrorDialog(
  529. this,
  530. apiYouTube->GetLastError().isEmpty()
  531. ? QTStr("YouTube.Actions.Error.General")
  532. : QTStr("YouTube.Actions.Error.Text")
  533. .arg(apiYouTube->GetLastError()));
  534. return;
  535. }
  536. //https://studio.youtube.com/channel/UCA9bSfH3KL186kyiUsvi3IA/videos/live?filter=%5B%5D&sort=%7B%22columnType%22%3A%22date%22%2C%22sortOrder%22%3A%22DESCENDING%22%7D
  537. QString uri =
  538. QString("https://studio.youtube.com/channel/%1/videos/live?filter=[]&sort={\"columnType\"%3A\"date\"%2C\"sortOrder\"%3A\"DESCENDING\"}")
  539. .arg(channel.id);
  540. QDesktopServices::openUrl(uri);
  541. }
  542. void OBSYoutubeActions::Cancel()
  543. {
  544. workerThread->stop();
  545. reject();
  546. }
  547. void OBSYoutubeActions::Accept()
  548. {
  549. workerThread->stop();
  550. accept();
  551. }