mirror of
https://github.com/obsproject/obs-studio.git
synced 2024-09-20 04:42:18 +02:00
710d99ef4d
When a source file contains an explicit include with a filename following the "moc_<actual-filename>.cpp" pattern, then CMake's AUTOMOC generation tool will recognize the matching pair and generate the replacement header file and add the required include directory entries. For all files which do contain Q_OBJECT or similar declarations but do not have an explicit include directive, the global mocs_compilation.cpp file will still be generated (which groups all "missing" generated headers). The larger this global file is, the more expensive incremental compilation will be as this file (and all its contained generated headers) will be re-generated regardless of whether actual changes occurred.
619 lines
18 KiB
C++
619 lines
18 KiB
C++
#include "moc_youtube-api-wrappers.cpp"
|
|
|
|
#include <QUrl>
|
|
#include <QMimeDatabase>
|
|
#include <QFile>
|
|
|
|
#include <string>
|
|
#include <iostream>
|
|
#include <qt-wrappers.hpp>
|
|
|
|
#include "auth-youtube.hpp"
|
|
#include "obs-app.hpp"
|
|
#include "window-basic-main.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 IsUserSignedIntoYT()
|
|
{
|
|
Auth *auth = OBSBasic::Get()->GetAuth();
|
|
if (auth) {
|
|
YoutubeApiWrappers *apiYouTube(
|
|
dynamic_cast<YoutubeApiWrappers *>(auth));
|
|
if (apiYouTube) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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 = 60 + 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);
|
|
}
|