youtube-api-wrappers.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. #include "youtube-api-wrappers.hpp"
  2. #include <QUrl>
  3. #include <string>
  4. #include <iostream>
  5. #include "auth-youtube.hpp"
  6. #include "obs-app.hpp"
  7. #include "qt-wrappers.hpp"
  8. #include "remote-text.hpp"
  9. #include "ui-config.h"
  10. #include "obf.h"
  11. using namespace json11;
  12. /* ------------------------------------------------------------------------- */
  13. #define YOUTUBE_LIVE_API_URL "https://www.googleapis.com/youtube/v3"
  14. #define YOUTUBE_LIVE_STREAM_URL YOUTUBE_LIVE_API_URL "/liveStreams"
  15. #define YOUTUBE_LIVE_BROADCAST_URL YOUTUBE_LIVE_API_URL "/liveBroadcasts"
  16. #define YOUTUBE_LIVE_BROADCAST_TRANSITION_URL \
  17. YOUTUBE_LIVE_BROADCAST_URL "/transition"
  18. #define YOUTUBE_LIVE_BROADCAST_BIND_URL YOUTUBE_LIVE_BROADCAST_URL "/bind"
  19. #define YOUTUBE_LIVE_CHANNEL_URL YOUTUBE_LIVE_API_URL "/channels"
  20. #define YOUTUBE_LIVE_TOKEN_URL "https://oauth2.googleapis.com/token"
  21. #define YOUTUBE_LIVE_VIDEOCATEGORIES_URL YOUTUBE_LIVE_API_URL "/videoCategories"
  22. #define YOUTUBE_LIVE_VIDEOS_URL YOUTUBE_LIVE_API_URL "/videos"
  23. #define DEFAULT_BROADCASTS_PER_QUERY \
  24. "7" // acceptable values are 0 to 50, inclusive
  25. /* ------------------------------------------------------------------------- */
  26. bool IsYouTubeService(const std::string &service)
  27. {
  28. auto it = find_if(youtubeServices.begin(), youtubeServices.end(),
  29. [&service](const Auth::Def &yt) {
  30. return service == yt.service;
  31. });
  32. return it != youtubeServices.end();
  33. }
  34. YoutubeApiWrappers::YoutubeApiWrappers(const Def &d) : YoutubeAuth(d) {}
  35. bool YoutubeApiWrappers::TryInsertCommand(const char *url,
  36. const char *content_type,
  37. std::string request_type,
  38. const char *data, Json &json_out,
  39. long *error_code)
  40. {
  41. if (error_code)
  42. *error_code = 0;
  43. #ifdef _DEBUG
  44. blog(LOG_DEBUG, "YouTube API command URL: %s", url);
  45. blog(LOG_DEBUG, "YouTube API command data: %s", data);
  46. #endif
  47. if (token.empty())
  48. return false;
  49. std::string output;
  50. std::string error;
  51. bool success = GetRemoteFile(url, output, error, error_code,
  52. content_type, request_type, data,
  53. {"Authorization: Bearer " + token},
  54. nullptr, 5);
  55. if (!success || output.empty())
  56. return false;
  57. json_out = Json::parse(output, error);
  58. #ifdef _DEBUG
  59. blog(LOG_DEBUG, "YouTube API command answer: %s",
  60. json_out.dump().c_str());
  61. #endif
  62. if (!error.empty()) {
  63. return false;
  64. }
  65. return true;
  66. }
  67. bool YoutubeApiWrappers::UpdateAccessToken()
  68. {
  69. if (refresh_token.empty()) {
  70. return false;
  71. }
  72. std::string clientid = YOUTUBE_CLIENTID;
  73. std::string secret = YOUTUBE_SECRET;
  74. deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH);
  75. deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH);
  76. std::string r_token =
  77. QUrl::toPercentEncoding(refresh_token.c_str()).toStdString();
  78. const QString url = YOUTUBE_LIVE_TOKEN_URL;
  79. const QString data_template = "client_id=%1"
  80. "&client_secret=%2"
  81. "&refresh_token=%3"
  82. "&grant_type=refresh_token";
  83. const QString data = data_template.arg(QString(clientid.c_str()),
  84. QString(secret.c_str()),
  85. QString(r_token.c_str()));
  86. Json json_out;
  87. bool success = TryInsertCommand(QT_TO_UTF8(url),
  88. "application/x-www-form-urlencoded", "",
  89. QT_TO_UTF8(data), json_out);
  90. if (!success || json_out.object_items().find("error") !=
  91. json_out.object_items().end())
  92. return false;
  93. token = json_out["access_token"].string_value();
  94. return token.empty() ? false : true;
  95. }
  96. bool YoutubeApiWrappers::InsertCommand(const char *url,
  97. const char *content_type,
  98. std::string request_type,
  99. const char *data, Json &json_out)
  100. {
  101. long error_code;
  102. if (!TryInsertCommand(url, content_type, request_type, data, json_out,
  103. &error_code)) {
  104. if (error_code == 401) {
  105. if (!UpdateAccessToken()) {
  106. return false;
  107. }
  108. //The second try after update token.
  109. return TryInsertCommand(url, content_type, request_type,
  110. data, json_out);
  111. }
  112. return false;
  113. }
  114. if (json_out.object_items().find("error") !=
  115. json_out.object_items().end()) {
  116. lastError = json_out["error"]["code"].int_value();
  117. lastErrorMessage = QString(
  118. json_out["error"]["message"].string_value().c_str());
  119. if (json_out["error"]["code"] == 401) {
  120. if (!UpdateAccessToken()) {
  121. return false;
  122. }
  123. //The second try after update token.
  124. return TryInsertCommand(url, content_type, request_type,
  125. data, json_out);
  126. }
  127. return false;
  128. }
  129. return true;
  130. }
  131. bool YoutubeApiWrappers::GetChannelDescription(
  132. ChannelDescription &channel_description)
  133. {
  134. lastErrorMessage.clear();
  135. const QByteArray url = YOUTUBE_LIVE_CHANNEL_URL
  136. "?part=snippet,contentDetails,statistics"
  137. "&mine=true";
  138. Json json_out;
  139. if (!InsertCommand(url, "application/json", "", nullptr, json_out)) {
  140. return false;
  141. }
  142. channel_description.id =
  143. QString(json_out["items"][0]["id"].string_value().c_str());
  144. channel_description.country =
  145. QString(json_out["items"][0]["snippet"]["country"]
  146. .string_value()
  147. .c_str());
  148. channel_description.language =
  149. QString(json_out["items"][0]["snippet"]["defaultLanguage"]
  150. .string_value()
  151. .c_str());
  152. channel_description.title = QString(
  153. json_out["items"][0]["snippet"]["title"].string_value().c_str());
  154. return channel_description.id.isEmpty() ? false : true;
  155. }
  156. bool YoutubeApiWrappers::InsertBroadcast(BroadcastDescription &broadcast)
  157. {
  158. // Youtube API: The Title property's value must be between 1 and 100 characters long.
  159. if (broadcast.title.isEmpty() || broadcast.title.length() > 100) {
  160. blog(LOG_ERROR, "Insert broadcast FAIL: Wrong title.");
  161. lastErrorMessage = "Broadcast title too long.";
  162. return false;
  163. }
  164. // Youtube API: The property's value can contain up to 5000 characters.
  165. if (broadcast.description.length() > 5000) {
  166. blog(LOG_ERROR, "Insert broadcast FAIL: Description too long.");
  167. lastErrorMessage = "Broadcast description too long.";
  168. return false;
  169. }
  170. const QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
  171. "?part=snippet,status,contentDetails";
  172. const Json data = Json::object{
  173. {"snippet",
  174. Json::object{
  175. {"title", QT_TO_UTF8(broadcast.title)},
  176. {"description", QT_TO_UTF8(broadcast.description)},
  177. {"scheduledStartTime",
  178. QT_TO_UTF8(broadcast.schedul_date_time)},
  179. }},
  180. {"status",
  181. Json::object{
  182. {"privacyStatus", QT_TO_UTF8(broadcast.privacy)},
  183. {"selfDeclaredMadeForKids", broadcast.made_for_kids},
  184. }},
  185. {"contentDetails",
  186. Json::object{
  187. {"latencyPreference", QT_TO_UTF8(broadcast.latency)},
  188. {"enableAutoStart", broadcast.auto_start},
  189. {"enableAutoStop", broadcast.auto_stop},
  190. {"enableDvr", broadcast.dvr},
  191. {"projection", QT_TO_UTF8(broadcast.projection)},
  192. {
  193. "monitorStream",
  194. Json::object{
  195. {"enableMonitorStream", false},
  196. },
  197. },
  198. }},
  199. };
  200. Json json_out;
  201. if (!InsertCommand(url, "application/json", "", data.dump().c_str(),
  202. json_out)) {
  203. return false;
  204. }
  205. broadcast.id = QString(json_out["id"].string_value().c_str());
  206. return broadcast.id.isEmpty() ? false : true;
  207. }
  208. bool YoutubeApiWrappers::InsertStream(StreamDescription &stream)
  209. {
  210. // Youtube API documentation: The snippet.title property's value in the liveStream resource must be between 1 and 128 characters long.
  211. if (stream.title.isEmpty() || stream.title.length() > 128) {
  212. blog(LOG_ERROR, "Insert stream FAIL: wrong argument");
  213. return false;
  214. }
  215. // Youtube API: The snippet.description property's value in the liveStream resource can have up to 10000 characters.
  216. if (stream.description.length() > 10000) {
  217. blog(LOG_ERROR, "Insert stream FAIL: Description too long.");
  218. return false;
  219. }
  220. const QByteArray url = YOUTUBE_LIVE_STREAM_URL
  221. "?part=snippet,cdn,status,contentDetails";
  222. const Json data = Json::object{
  223. {"snippet",
  224. Json::object{
  225. {"title", QT_TO_UTF8(stream.title)},
  226. }},
  227. {"cdn",
  228. Json::object{
  229. {"frameRate", "variable"},
  230. {"ingestionType", "rtmp"},
  231. {"resolution", "variable"},
  232. }},
  233. {"contentDetails", Json::object{{"isReusable", false}}},
  234. };
  235. Json json_out;
  236. if (!InsertCommand(url, "application/json", "", data.dump().c_str(),
  237. json_out)) {
  238. return false;
  239. }
  240. stream.id = QString(json_out["id"].string_value().c_str());
  241. stream.name = QString(json_out["cdn"]["ingestionInfo"]["streamName"]
  242. .string_value()
  243. .c_str());
  244. return stream.id.isEmpty() ? false : true;
  245. }
  246. bool YoutubeApiWrappers::BindStream(const QString broadcast_id,
  247. const QString stream_id)
  248. {
  249. const QString url_template = YOUTUBE_LIVE_BROADCAST_BIND_URL
  250. "?id=%1"
  251. "&streamId=%2"
  252. "&part=id,snippet,contentDetails,status";
  253. const QString url = url_template.arg(broadcast_id, stream_id);
  254. const Json data = Json::object{};
  255. this->broadcast_id = broadcast_id;
  256. Json json_out;
  257. return InsertCommand(QT_TO_UTF8(url), "application/json", "",
  258. data.dump().c_str(), json_out);
  259. }
  260. bool YoutubeApiWrappers::GetBroadcastsList(Json &json_out, QString page)
  261. {
  262. lastErrorMessage.clear();
  263. QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
  264. "?part=snippet,contentDetails,status"
  265. "&broadcastType=all&maxResults=" DEFAULT_BROADCASTS_PER_QUERY
  266. "&mine=true";
  267. if (!page.isEmpty())
  268. url += "&pageToken=" + page.toUtf8();
  269. return InsertCommand(url, "application/json", "", nullptr, json_out);
  270. }
  271. bool YoutubeApiWrappers::GetVideoCategoriesList(
  272. const QString &country, const QString &language,
  273. QVector<CategoryDescription> &category_list_out)
  274. {
  275. const QString url_template = YOUTUBE_LIVE_VIDEOCATEGORIES_URL
  276. "?part=snippet"
  277. "&regionCode=%1"
  278. "&hl=%2";
  279. const QString url =
  280. url_template.arg(country.isEmpty() ? "US" : country,
  281. language.isEmpty() ? "en" : language);
  282. Json json_out;
  283. if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr,
  284. json_out)) {
  285. return false;
  286. }
  287. category_list_out = {};
  288. for (auto &j : json_out["items"].array_items()) {
  289. // Assignable only.
  290. if (j["snippet"]["assignable"].bool_value()) {
  291. category_list_out.push_back(
  292. {j["id"].string_value().c_str(),
  293. j["snippet"]["title"].string_value().c_str()});
  294. }
  295. }
  296. return category_list_out.isEmpty() ? false : true;
  297. }
  298. bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id,
  299. const QString &video_title,
  300. const QString &video_description,
  301. const QString &categorie_id)
  302. {
  303. const QByteArray url = YOUTUBE_LIVE_VIDEOS_URL "?part=snippet";
  304. const Json data = Json::object{
  305. {"id", QT_TO_UTF8(video_id)},
  306. {"snippet",
  307. Json::object{
  308. {"title", QT_TO_UTF8(video_title)},
  309. {"description", QT_TO_UTF8(video_description)},
  310. {"categoryId", QT_TO_UTF8(categorie_id)},
  311. }},
  312. };
  313. Json json_out;
  314. return InsertCommand(url, "application/json", "PUT",
  315. data.dump().c_str(), json_out);
  316. }
  317. bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id)
  318. {
  319. lastErrorMessage.clear();
  320. if (!ResetBroadcast(broadcast_id))
  321. return false;
  322. const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
  323. "?id=%1"
  324. "&broadcastStatus=%2"
  325. "&part=status";
  326. const QString live = url_template.arg(broadcast_id, "live");
  327. Json json_out;
  328. return InsertCommand(QT_TO_UTF8(live), "application/json", "POST", "{}",
  329. json_out);
  330. }
  331. bool YoutubeApiWrappers::StartLatestBroadcast()
  332. {
  333. return StartBroadcast(this->broadcast_id);
  334. }
  335. bool YoutubeApiWrappers::StopBroadcast(const QString &broadcast_id)
  336. {
  337. lastErrorMessage.clear();
  338. const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
  339. "?id=%1"
  340. "&broadcastStatus=complete"
  341. "&part=status";
  342. const QString url = url_template.arg(broadcast_id);
  343. Json json_out;
  344. return InsertCommand(QT_TO_UTF8(url), "application/json", "POST", "{}",
  345. json_out);
  346. }
  347. bool YoutubeApiWrappers::StopLatestBroadcast()
  348. {
  349. return StopBroadcast(this->broadcast_id);
  350. }
  351. bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id)
  352. {
  353. lastErrorMessage.clear();
  354. const QString url_template = YOUTUBE_LIVE_BROADCAST_URL
  355. "?part=id,snippet,contentDetails,status"
  356. "&id=%1";
  357. const QString url = url_template.arg(broadcast_id);
  358. Json json_out;
  359. if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr,
  360. json_out))
  361. return false;
  362. const QString put = YOUTUBE_LIVE_BROADCAST_URL
  363. "?part=id,snippet,contentDetails,status";
  364. auto snippet = json_out["items"][0]["snippet"];
  365. auto status = json_out["items"][0]["status"];
  366. auto contentDetails = json_out["items"][0]["contentDetails"];
  367. auto monitorStream = contentDetails["monitorStream"];
  368. const Json data = Json::object{
  369. {"id", QT_TO_UTF8(broadcast_id)},
  370. {"snippet",
  371. Json::object{
  372. {"title", snippet["title"]},
  373. {"scheduledStartTime", snippet["scheduledStartTime"]},
  374. }},
  375. {"status",
  376. Json::object{
  377. {"privacyStatus", status["privacyStatus"]},
  378. {"madeForKids", status["madeForKids"]},
  379. {"selfDeclaredMadeForKids",
  380. status["selfDeclaredMadeForKids"]},
  381. }},
  382. {"contentDetails",
  383. Json::object{
  384. {
  385. "monitorStream",
  386. Json::object{
  387. {"enableMonitorStream", false},
  388. {"broadcastStreamDelayMs",
  389. monitorStream["broadcastStreamDelayMs"]},
  390. },
  391. },
  392. {"enableDvr", contentDetails["enableDvr"]},
  393. {"enableContentEncryption",
  394. contentDetails["enableContentEncryption"]},
  395. {"enableEmbed", contentDetails["enableEmbed"]},
  396. {"recordFromStart", contentDetails["recordFromStart"]},
  397. {"startWithSlate", contentDetails["startWithSlate"]},
  398. }},
  399. };
  400. return InsertCommand(QT_TO_UTF8(put), "application/json", "PUT",
  401. data.dump().c_str(), json_out);
  402. }
  403. bool YoutubeApiWrappers::FindBroadcast(const QString &id,
  404. json11::Json &json_out)
  405. {
  406. lastErrorMessage.clear();
  407. QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
  408. "?part=id,snippet,contentDetails,status"
  409. "&broadcastType=all&maxResults=1";
  410. url += "&id=" + id.toUtf8();
  411. if (!InsertCommand(url, "application/json", "", nullptr, json_out))
  412. return false;
  413. auto items = json_out["items"].array_items();
  414. if (items.size() != 1) {
  415. lastErrorMessage = "No active broadcast found.";
  416. return false;
  417. }
  418. return true;
  419. }
  420. bool YoutubeApiWrappers::FindStream(const QString &id, json11::Json &json_out)
  421. {
  422. lastErrorMessage.clear();
  423. QByteArray url = YOUTUBE_LIVE_STREAM_URL "?part=id,snippet,cdn,status"
  424. "&maxResults=1";
  425. url += "&id=" + id.toUtf8();
  426. if (!InsertCommand(url, "application/json", "", nullptr, json_out))
  427. return false;
  428. auto items = json_out["items"].array_items();
  429. if (items.size() != 1) {
  430. lastErrorMessage = "No active broadcast found.";
  431. return false;
  432. }
  433. return true;
  434. }