window-youtube-actions.cpp 18 KB

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