window-youtube-actions.cpp 18 KB

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