| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 | 
							- #include "youtube-api-wrappers.hpp"
 
- #include <QUrl>
 
- #include <QMimeDatabase>
 
- #include <QFile>
 
- #include <string>
 
- #include <iostream>
 
- #include "auth-youtube.hpp"
 
- #include "obs-app.hpp"
 
- #include "qt-wrappers.hpp"
 
- #include "remote-text.hpp"
 
- #include "ui-config.h"
 
- #include "obf.h"
 
- using namespace json11;
 
- /* ------------------------------------------------------------------------- */
 
- #define YOUTUBE_LIVE_API_URL "https://www.googleapis.com/youtube/v3"
 
- #define YOUTUBE_LIVE_STREAM_URL YOUTUBE_LIVE_API_URL "/liveStreams"
 
- #define YOUTUBE_LIVE_BROADCAST_URL YOUTUBE_LIVE_API_URL "/liveBroadcasts"
 
- #define YOUTUBE_LIVE_BROADCAST_TRANSITION_URL \
 
- 	YOUTUBE_LIVE_BROADCAST_URL "/transition"
 
- #define YOUTUBE_LIVE_BROADCAST_BIND_URL YOUTUBE_LIVE_BROADCAST_URL "/bind"
 
- #define YOUTUBE_LIVE_CHANNEL_URL YOUTUBE_LIVE_API_URL "/channels"
 
- #define YOUTUBE_LIVE_TOKEN_URL "https://oauth2.googleapis.com/token"
 
- #define YOUTUBE_LIVE_VIDEOCATEGORIES_URL YOUTUBE_LIVE_API_URL "/videoCategories"
 
- #define YOUTUBE_LIVE_VIDEOS_URL YOUTUBE_LIVE_API_URL "/videos"
 
- #define YOUTUBE_LIVE_CHAT_MESSAGES_URL YOUTUBE_LIVE_API_URL "/liveChat/messages"
 
- #define YOUTUBE_LIVE_THUMBNAIL_URL \
 
- 	"https://www.googleapis.com/upload/youtube/v3/thumbnails/set"
 
- #define DEFAULT_BROADCASTS_PER_QUERY \
 
- 	"50" // acceptable values are 0 to 50, inclusive
 
- /* ------------------------------------------------------------------------- */
 
- bool IsYouTubeService(const std::string &service)
 
- {
 
- 	auto it = find_if(youtubeServices.begin(), youtubeServices.end(),
 
- 			  [&service](const Auth::Def &yt) {
 
- 				  return service == yt.service;
 
- 			  });
 
- 	return it != youtubeServices.end();
 
- }
 
- bool YoutubeApiWrappers::GetTranslatedError(QString &error_message)
 
- {
 
- 	QString translated =
 
- 		QTStr("YouTube.Errors." + lastErrorReason.toUtf8());
 
- 	// No translation found
 
- 	if (translated.startsWith("YouTube.Errors."))
 
- 		return false;
 
- 	error_message = translated;
 
- 	return true;
 
- }
 
- YoutubeApiWrappers::YoutubeApiWrappers(const Def &d) : YoutubeAuth(d) {}
 
- bool YoutubeApiWrappers::TryInsertCommand(const char *url,
 
- 					  const char *content_type,
 
- 					  std::string request_type,
 
- 					  const char *data, Json &json_out,
 
- 					  long *error_code, int data_size)
 
- {
 
- 	long httpStatusCode = 0;
 
- #ifdef _DEBUG
 
- 	blog(LOG_DEBUG, "YouTube API command URL: %s", url);
 
- 	if (data && data[0] == '{') // only log JSON data
 
- 		blog(LOG_DEBUG, "YouTube API command data: %s", data);
 
- #endif
 
- 	if (token.empty())
 
- 		return false;
 
- 	std::string output;
 
- 	std::string error;
 
- 	// Increase timeout by the time it takes to transfer `data_size` at 1 Mbps
 
- 	int timeout = 5 + data_size / 125000;
 
- 	bool success = GetRemoteFile(url, output, error, &httpStatusCode,
 
- 				     content_type, request_type, data,
 
- 				     {"Authorization: Bearer " + token},
 
- 				     nullptr, timeout, false, data_size);
 
- 	if (error_code)
 
- 		*error_code = httpStatusCode;
 
- 	if (!success || output.empty()) {
 
- 		if (!error.empty())
 
- 			blog(LOG_WARNING, "YouTube API request failed: %s",
 
- 			     error.c_str());
 
- 		return false;
 
- 	}
 
- 	json_out = Json::parse(output, error);
 
- #ifdef _DEBUG
 
- 	blog(LOG_DEBUG, "YouTube API command answer: %s",
 
- 	     json_out.dump().c_str());
 
- #endif
 
- 	if (!error.empty()) {
 
- 		return false;
 
- 	}
 
- 	return httpStatusCode < 400;
 
- }
 
- bool YoutubeApiWrappers::UpdateAccessToken()
 
- {
 
- 	if (refresh_token.empty()) {
 
- 		return false;
 
- 	}
 
- 	std::string clientid = YOUTUBE_CLIENTID;
 
- 	std::string secret = YOUTUBE_SECRET;
 
- 	deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH);
 
- 	deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH);
 
- 	std::string r_token =
 
- 		QUrl::toPercentEncoding(refresh_token.c_str()).toStdString();
 
- 	const QString url = YOUTUBE_LIVE_TOKEN_URL;
 
- 	const QString data_template = "client_id=%1"
 
- 				      "&client_secret=%2"
 
- 				      "&refresh_token=%3"
 
- 				      "&grant_type=refresh_token";
 
- 	const QString data = data_template.arg(QString(clientid.c_str()),
 
- 					       QString(secret.c_str()),
 
- 					       QString(r_token.c_str()));
 
- 	Json json_out;
 
- 	bool success = TryInsertCommand(QT_TO_UTF8(url),
 
- 					"application/x-www-form-urlencoded", "",
 
- 					QT_TO_UTF8(data), json_out);
 
- 	if (!success || json_out.object_items().find("error") !=
 
- 				json_out.object_items().end())
 
- 		return false;
 
- 	token = json_out["access_token"].string_value();
 
- 	return token.empty() ? false : true;
 
- }
 
- bool YoutubeApiWrappers::InsertCommand(const char *url,
 
- 				       const char *content_type,
 
- 				       std::string request_type,
 
- 				       const char *data, Json &json_out,
 
- 				       int data_size)
 
- {
 
- 	long error_code;
 
- 	bool success = TryInsertCommand(url, content_type, request_type, data,
 
- 					json_out, &error_code, data_size);
 
- 	if (error_code == 401) {
 
- 		// Attempt to update access token and try again
 
- 		if (!UpdateAccessToken())
 
- 			return false;
 
- 		success = TryInsertCommand(url, content_type, request_type,
 
- 					   data, json_out, &error_code,
 
- 					   data_size);
 
- 	}
 
- 	if (json_out.object_items().find("error") !=
 
- 	    json_out.object_items().end()) {
 
- 		blog(LOG_ERROR,
 
- 		     "YouTube API error:\n\tHTTP status: %ld\n\tURL: %s\n\tJSON: %s",
 
- 		     error_code, url, json_out.dump().c_str());
 
- 		lastError = json_out["error"]["code"].int_value();
 
- 		lastErrorReason =
 
- 			QString(json_out["error"]["errors"][0]["reason"]
 
- 					.string_value()
 
- 					.c_str());
 
- 		lastErrorMessage = QString(
 
- 			json_out["error"]["message"].string_value().c_str());
 
- 		// The existence of an error implies non-success even if the HTTP status code disagrees.
 
- 		success = false;
 
- 	}
 
- 	return success;
 
- }
 
- bool YoutubeApiWrappers::GetChannelDescription(
 
- 	ChannelDescription &channel_description)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QByteArray url = YOUTUBE_LIVE_CHANNEL_URL
 
- 		"?part=snippet,contentDetails,statistics"
 
- 		"&mine=true";
 
- 	Json json_out;
 
- 	if (!InsertCommand(url, "application/json", "", nullptr, json_out)) {
 
- 		return false;
 
- 	}
 
- 	if (json_out["pageInfo"]["totalResults"].int_value() == 0) {
 
- 		lastErrorMessage = QTStr("YouTube.Auth.NoChannels");
 
- 		return false;
 
- 	}
 
- 	channel_description.id =
 
- 		QString(json_out["items"][0]["id"].string_value().c_str());
 
- 	channel_description.title = QString(
 
- 		json_out["items"][0]["snippet"]["title"].string_value().c_str());
 
- 	return channel_description.id.isEmpty() ? false : true;
 
- }
 
- bool YoutubeApiWrappers::InsertBroadcast(BroadcastDescription &broadcast)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
 
- 		"?part=snippet,status,contentDetails";
 
- 	const Json data = Json::object{
 
- 		{"snippet",
 
- 		 Json::object{
 
- 			 {"title", QT_TO_UTF8(broadcast.title)},
 
- 			 {"description", QT_TO_UTF8(broadcast.description)},
 
- 			 {"scheduledStartTime",
 
- 			  QT_TO_UTF8(broadcast.schedul_date_time)},
 
- 		 }},
 
- 		{"status",
 
- 		 Json::object{
 
- 			 {"privacyStatus", QT_TO_UTF8(broadcast.privacy)},
 
- 			 {"selfDeclaredMadeForKids", broadcast.made_for_kids},
 
- 		 }},
 
- 		{"contentDetails",
 
- 		 Json::object{
 
- 			 {"latencyPreference", QT_TO_UTF8(broadcast.latency)},
 
- 			 {"enableAutoStart", broadcast.auto_start},
 
- 			 {"enableAutoStop", broadcast.auto_stop},
 
- 			 {"enableDvr", broadcast.dvr},
 
- 			 {"projection", QT_TO_UTF8(broadcast.projection)},
 
- 			 {
 
- 				 "monitorStream",
 
- 				 Json::object{
 
- 					 {"enableMonitorStream", false},
 
- 				 },
 
- 			 },
 
- 		 }},
 
- 	};
 
- 	Json json_out;
 
- 	if (!InsertCommand(url, "application/json", "", data.dump().c_str(),
 
- 			   json_out)) {
 
- 		return false;
 
- 	}
 
- 	broadcast.id = QString(json_out["id"].string_value().c_str());
 
- 	return broadcast.id.isEmpty() ? false : true;
 
- }
 
- bool YoutubeApiWrappers::InsertStream(StreamDescription &stream)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QByteArray url = YOUTUBE_LIVE_STREAM_URL
 
- 		"?part=snippet,cdn,status,contentDetails";
 
- 	const Json data = Json::object{
 
- 		{"snippet",
 
- 		 Json::object{
 
- 			 {"title", QT_TO_UTF8(stream.title)},
 
- 		 }},
 
- 		{"cdn",
 
- 		 Json::object{
 
- 			 {"frameRate", "variable"},
 
- 			 {"ingestionType", "rtmp"},
 
- 			 {"resolution", "variable"},
 
- 		 }},
 
- 		{"contentDetails", Json::object{{"isReusable", false}}},
 
- 	};
 
- 	Json json_out;
 
- 	if (!InsertCommand(url, "application/json", "", data.dump().c_str(),
 
- 			   json_out)) {
 
- 		return false;
 
- 	}
 
- 	stream.id = QString(json_out["id"].string_value().c_str());
 
- 	stream.name = QString(json_out["cdn"]["ingestionInfo"]["streamName"]
 
- 				      .string_value()
 
- 				      .c_str());
 
- 	return stream.id.isEmpty() ? false : true;
 
- }
 
- bool YoutubeApiWrappers::BindStream(const QString broadcast_id,
 
- 				    const QString stream_id,
 
- 				    json11::Json &json_out)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QString url_template = YOUTUBE_LIVE_BROADCAST_BIND_URL
 
- 		"?id=%1"
 
- 		"&streamId=%2"
 
- 		"&part=id,snippet,contentDetails,status";
 
- 	const QString url = url_template.arg(broadcast_id, stream_id);
 
- 	const Json data = Json::object{};
 
- 	this->broadcast_id = broadcast_id;
 
- 	return InsertCommand(QT_TO_UTF8(url), "application/json", "",
 
- 			     data.dump().c_str(), json_out);
 
- }
 
- bool YoutubeApiWrappers::GetBroadcastsList(Json &json_out, const QString &page,
 
- 					   const QString &status)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
 
- 		"?part=snippet,contentDetails,status"
 
- 		"&broadcastType=all&maxResults=" DEFAULT_BROADCASTS_PER_QUERY;
 
- 	if (status.isEmpty())
 
- 		url += "&mine=true";
 
- 	else
 
- 		url += "&broadcastStatus=" + status.toUtf8();
 
- 	if (!page.isEmpty())
 
- 		url += "&pageToken=" + page.toUtf8();
 
- 	return InsertCommand(url, "application/json", "", nullptr, json_out);
 
- }
 
- bool YoutubeApiWrappers::GetVideoCategoriesList(
 
- 	QVector<CategoryDescription> &category_list_out)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QString url_template = YOUTUBE_LIVE_VIDEOCATEGORIES_URL
 
- 		"?part=snippet"
 
- 		"®ionCode=%1"
 
- 		"&hl=%2";
 
- 	/*
 
- 	 * All OBS locale regions aside from "US" are missing category id 29
 
- 	 * ("Nonprofits & Activism"), but it is still available to channels
 
- 	 * set to those regions via the YouTube Studio website.
 
- 	 * To work around this inconsistency with the API all locales will
 
- 	 * use the "US" region and only set the language part for localisation.
 
- 	 * It is worth noting that none of the regions available on YouTube
 
- 	 * feature any category not also available to the "US" region.
 
- 	 */
 
- 	QString url = url_template.arg("US", QLocale().name());
 
- 	Json json_out;
 
- 	if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr,
 
- 			   json_out)) {
 
- 		if (lastErrorReason != "unsupportedLanguageCode" &&
 
- 		    lastErrorReason != "invalidLanguage")
 
- 			return false;
 
- 		// Try again with en-US if YouTube error indicates an unsupported locale
 
- 		url = url_template.arg("US", "en_US");
 
- 		if (!InsertCommand(QT_TO_UTF8(url), "application/json", "",
 
- 				   nullptr, json_out))
 
- 			return false;
 
- 	}
 
- 	category_list_out = {};
 
- 	for (auto &j : json_out["items"].array_items()) {
 
- 		// Assignable only.
 
- 		if (j["snippet"]["assignable"].bool_value()) {
 
- 			category_list_out.push_back(
 
- 				{j["id"].string_value().c_str(),
 
- 				 j["snippet"]["title"].string_value().c_str()});
 
- 		}
 
- 	}
 
- 	return category_list_out.isEmpty() ? false : true;
 
- }
 
- bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id,
 
- 					  const QString &video_title,
 
- 					  const QString &video_description,
 
- 					  const QString &categorie_id)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QByteArray url = YOUTUBE_LIVE_VIDEOS_URL "?part=snippet";
 
- 	const Json data = Json::object{
 
- 		{"id", QT_TO_UTF8(video_id)},
 
- 		{"snippet",
 
- 		 Json::object{
 
- 			 {"title", QT_TO_UTF8(video_title)},
 
- 			 {"description", QT_TO_UTF8(video_description)},
 
- 			 {"categoryId", QT_TO_UTF8(categorie_id)},
 
- 		 }},
 
- 	};
 
- 	Json json_out;
 
- 	return InsertCommand(url, "application/json", "PUT",
 
- 			     data.dump().c_str(), json_out);
 
- }
 
- bool YoutubeApiWrappers::SetVideoThumbnail(const QString &video_id,
 
- 					   const QString &thumbnail_file)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	// Make sure the file hasn't been deleted since originally selecting it
 
- 	if (!QFile::exists(thumbnail_file)) {
 
- 		lastErrorMessage = QTStr("YouTube.Actions.Error.FileMissing");
 
- 		return false;
 
- 	}
 
- 	QFile thumbFile(thumbnail_file);
 
- 	if (!thumbFile.open(QFile::ReadOnly)) {
 
- 		lastErrorMessage =
 
- 			QTStr("YouTube.Actions.Error.FileOpeningFailed");
 
- 		return false;
 
- 	}
 
- 	const QByteArray fileContents = thumbFile.readAll();
 
- 	const QString mime =
 
- 		QMimeDatabase().mimeTypeForData(fileContents).name();
 
- 	const QString url = YOUTUBE_LIVE_THUMBNAIL_URL "?videoId=" + video_id;
 
- 	Json json_out;
 
- 	return InsertCommand(QT_TO_UTF8(url), QT_TO_UTF8(mime), "POST",
 
- 			     fileContents.constData(), json_out,
 
- 			     fileContents.size());
 
- }
 
- bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	Json json_out;
 
- 	if (!FindBroadcast(broadcast_id, json_out))
 
- 		return false;
 
- 	auto lifeCycleStatus =
 
- 		json_out["items"][0]["status"]["lifeCycleStatus"].string_value();
 
- 	if (lifeCycleStatus == "live" || lifeCycleStatus == "liveStarting")
 
- 		// Broadcast is already (going to be) live
 
- 		return true;
 
- 	else if (lifeCycleStatus == "testStarting") {
 
- 		// User will need to wait a few seconds before attempting to start broadcast
 
- 		lastErrorMessage =
 
- 			QTStr("YouTube.Actions.Error.BroadcastTestStarting");
 
- 		lastErrorReason.clear();
 
- 		return false;
 
- 	}
 
- 	// Only reset if broadcast has monitoring enabled and is not already in "testing" mode
 
- 	auto monitorStreamEnabled =
 
- 		json_out["items"][0]["contentDetails"]["monitorStream"]
 
- 			["enableMonitorStream"]
 
- 				.bool_value();
 
- 	if (lifeCycleStatus != "testing" && monitorStreamEnabled &&
 
- 	    !ResetBroadcast(broadcast_id, json_out))
 
- 		return false;
 
- 	const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
 
- 		"?id=%1"
 
- 		"&broadcastStatus=%2"
 
- 		"&part=status";
 
- 	const QString live = url_template.arg(broadcast_id, "live");
 
- 	bool success = InsertCommand(QT_TO_UTF8(live), "application/json",
 
- 				     "POST", "{}", json_out);
 
- 	// Return a success if the command failed, but was redundant (broadcast already live)
 
- 	return success || lastErrorReason == "redundantTransition";
 
- }
 
- bool YoutubeApiWrappers::StartLatestBroadcast()
 
- {
 
- 	return StartBroadcast(this->broadcast_id);
 
- }
 
- bool YoutubeApiWrappers::StopBroadcast(const QString &broadcast_id)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL
 
- 		"?id=%1"
 
- 		"&broadcastStatus=complete"
 
- 		"&part=status";
 
- 	const QString url = url_template.arg(broadcast_id);
 
- 	Json json_out;
 
- 	bool success = InsertCommand(QT_TO_UTF8(url), "application/json",
 
- 				     "POST", "{}", json_out);
 
- 	// Return a success if the command failed, but was redundant (broadcast already stopped)
 
- 	return success || lastErrorReason == "redundantTransition";
 
- }
 
- bool YoutubeApiWrappers::StopLatestBroadcast()
 
- {
 
- 	return StopBroadcast(this->broadcast_id);
 
- }
 
- void YoutubeApiWrappers::SetBroadcastId(QString &broadcast_id)
 
- {
 
- 	this->broadcast_id = broadcast_id;
 
- }
 
- QString YoutubeApiWrappers::GetBroadcastId()
 
- {
 
- 	return this->broadcast_id;
 
- }
 
- bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id,
 
- 					json11::Json &json_out)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	auto snippet = json_out["items"][0]["snippet"];
 
- 	auto status = json_out["items"][0]["status"];
 
- 	auto contentDetails = json_out["items"][0]["contentDetails"];
 
- 	auto monitorStream = contentDetails["monitorStream"];
 
- 	const Json data = Json::object{
 
- 		{"id", QT_TO_UTF8(broadcast_id)},
 
- 		{"snippet",
 
- 		 Json::object{
 
- 			 {"title", snippet["title"]},
 
- 			 {"description", snippet["description"]},
 
- 			 {"scheduledStartTime", snippet["scheduledStartTime"]},
 
- 			 {"scheduledEndTime", snippet["scheduledEndTime"]},
 
- 		 }},
 
- 		{"status",
 
- 		 Json::object{
 
- 			 {"privacyStatus", status["privacyStatus"]},
 
- 			 {"madeForKids", status["madeForKids"]},
 
- 			 {"selfDeclaredMadeForKids",
 
- 			  status["selfDeclaredMadeForKids"]},
 
- 		 }},
 
- 		{"contentDetails",
 
- 		 Json::object{
 
- 			 {
 
- 				 "monitorStream",
 
- 				 Json::object{
 
- 					 {"enableMonitorStream", false},
 
- 					 {"broadcastStreamDelayMs",
 
- 					  monitorStream["broadcastStreamDelayMs"]},
 
- 				 },
 
- 			 },
 
- 			 {"enableAutoStart", contentDetails["enableAutoStart"]},
 
- 			 {"enableAutoStop", contentDetails["enableAutoStop"]},
 
- 			 {"enableClosedCaptions",
 
- 			  contentDetails["enableClosedCaptions"]},
 
- 			 {"enableDvr", contentDetails["enableDvr"]},
 
- 			 {"enableContentEncryption",
 
- 			  contentDetails["enableContentEncryption"]},
 
- 			 {"enableEmbed", contentDetails["enableEmbed"]},
 
- 			 {"recordFromStart", contentDetails["recordFromStart"]},
 
- 			 {"startWithSlate", contentDetails["startWithSlate"]},
 
- 		 }},
 
- 	};
 
- 	const QString put = YOUTUBE_LIVE_BROADCAST_URL
 
- 		"?part=id,snippet,contentDetails,status";
 
- 	return InsertCommand(QT_TO_UTF8(put), "application/json", "PUT",
 
- 			     data.dump().c_str(), json_out);
 
- }
 
- bool YoutubeApiWrappers::FindBroadcast(const QString &id,
 
- 				       json11::Json &json_out)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	QByteArray url = YOUTUBE_LIVE_BROADCAST_URL
 
- 		"?part=id,snippet,contentDetails,status"
 
- 		"&broadcastType=all&maxResults=1";
 
- 	url += "&id=" + id.toUtf8();
 
- 	if (!InsertCommand(url, "application/json", "", nullptr, json_out))
 
- 		return false;
 
- 	auto items = json_out["items"].array_items();
 
- 	if (items.size() != 1) {
 
- 		lastErrorMessage =
 
- 			QTStr("YouTube.Actions.Error.BroadcastNotFound");
 
- 		return false;
 
- 	}
 
- 	return true;
 
- }
 
- bool YoutubeApiWrappers::FindStream(const QString &id, json11::Json &json_out)
 
- {
 
- 	lastErrorMessage.clear();
 
- 	lastErrorReason.clear();
 
- 	QByteArray url = YOUTUBE_LIVE_STREAM_URL "?part=id,snippet,cdn,status"
 
- 						 "&maxResults=1";
 
- 	url += "&id=" + id.toUtf8();
 
- 	if (!InsertCommand(url, "application/json", "", nullptr, json_out))
 
- 		return false;
 
- 	auto items = json_out["items"].array_items();
 
- 	if (items.size() != 1) {
 
- 		lastErrorMessage = "No active broadcast found.";
 
- 		return false;
 
- 	}
 
- 	return true;
 
- }
 
- bool YoutubeApiWrappers::SendChatMessage(const std::string &chat_id,
 
- 					 const QString &message)
 
- {
 
- 	QByteArray url = YOUTUBE_LIVE_CHAT_MESSAGES_URL "?part=snippet";
 
- 	json11::Json json_in = Json::object{
 
- 		{"snippet",
 
- 		 Json::object{
 
- 			 {"liveChatId", chat_id},
 
- 			 {"type", "textMessageEvent"},
 
- 			 {"textMessageDetails",
 
- 			  Json::object{{"messageText", QT_TO_UTF8(message)}}},
 
- 		 }}};
 
- 	json11::Json json_out;
 
- 	return InsertCommand(url, "application/json", "POST",
 
- 			     json_in.dump().c_str(), json_out);
 
- }
 
 
  |