Last active
February 15, 2024 16:40
-
-
Save forderud/941e1d2014c43a0e3f5f0af1d7972075 to your computer and use it in GitHub Desktop.
YouTube video upload from Qt/C++
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include <QObject> | |
#include <QOAuth2AuthorizationCodeFlow> | |
/** YouTube video upload class. | |
Require API key & OAuth 2.0 client ID from https://console.developers.google.com/apis/credentials */ | |
class YouTube : public QObject { | |
Q_OBJECT | |
Q_PROPERTY(bool limited_auth MEMBER m_limited_auth) | |
Q_PROPERTY(QString api_file MEMBER m_api_file) | |
Q_PROPERTY(QString client_file MEMBER m_client_file) | |
Q_PROPERTY(QString title MEMBER m_title) | |
Q_PROPERTY(QString description MEMBER m_description) | |
Q_PROPERTY(QString category MEMBER m_category) | |
Q_PROPERTY(QStringList tags MEMBER m_tags) | |
public: | |
YouTube(QObject* parent = nullptr); | |
~YouTube() override; | |
Q_SLOT void UploadFile(QUrl filename); | |
/** Only called in limited_auth mode. */ | |
Q_SIGNAL void showLimitedAuthURL(QString verification_url, QString user_code); | |
/** Emitted if video upload fails. */ | |
Q_SIGNAL void failed(QString message); | |
/** Emitted if video upload succeeded. */ | |
Q_SIGNAL void succeeded(QUrl url); | |
bool m_limited_auth = false; ///< limited-input device authentication with login on secondary device (typ. mobile phone) | |
QString m_api_file; ///< file-path to text file containing API key | |
QString m_client_file; ///< file-path to JSON file containing client secrets | |
QString m_title; ///< video title | |
QString m_description; ///< video descriptions | |
QStringList m_tags; ///< | |
QString m_category; ///< see https://developers.google.com/youtube/v3/docs/videoCategories/list | |
private: | |
struct ClientID { | |
QString auth_uri; | |
QString client_id; | |
QString token_uri; | |
QString client_secret; | |
}; | |
static QByteArray LoadFile(QString filename); | |
/** Parse OAuth 2.0 Client ID JSON file */ | |
static ClientID ParseClientID(QString json_filename); | |
QOAuth2AuthorizationCodeFlow* InitOAuth(ClientID client, /*out,async*/QString& access_token) const; | |
/** https://developers.google.com/youtube/v3/docs/videos/insert */ | |
void VideoUploadRequest(); ///< step 1 | |
void VideoUploadData(QNetworkReply* reply); ///< step 2 | |
void VideoUploadResult(QNetworkReply* reply); ///< step 3 | |
/** https://developers.google.com/identity/protocols/oauth2/limited-input-device */ | |
void LimitedInputAuthRequest(); ///< step 1 | |
void LimitedInputAuthResponse(QNetworkReply* reply); ///< step 2 | |
void LimitedInputResultRequest(); ///< step 3 | |
void LimitedInputResultResponse(QNetworkReply* reply);///< step 4 | |
private: | |
QByteArray m_video_file_content; | |
QString m_access_token; | |
QString m_device_code; ///< for limited-input auth | |
int m_poll_interval = 0; ///< for limited-input auth | |
}; | |
--------------------------------------------------------------------------------------------- | |
#include <QOAuthHttpServerReplyHandler> | |
#include <QNetworkReply> | |
#include <QSslSocket> | |
#include <QUrlQuery> | |
#include <QJsonDocument> | |
#include <QJsonObject> | |
#include <QJsonArray> | |
#include <QQmlEngine> | |
#include <QFile> | |
#include <QDesktopServices> | |
#include <QTimer> | |
#include "YouTube.hpp" | |
static int s_unused_val = qmlRegisterType<YouTube>("sonotube", 1, 0, "YouTube"); | |
static const QString s_scope = "https://www.googleapis.com/auth/youtube.upload"; | |
QByteArray YouTube::LoadFile(QString filename) { | |
QFile file(filename); | |
file.open(QIODevice::ReadOnly); | |
return file.readAll(); | |
} | |
YouTube::ClientID YouTube::ParseClientID(QString json_filename) { | |
QFile file(json_filename); | |
file.open(QIODevice::ReadOnly | QIODevice::Text); | |
QByteArray content = file.readAll(); | |
QJsonObject object = QJsonDocument::fromJson(content).object(); | |
const auto settingsObject = object["installed"].toObject(); | |
ClientID id; | |
id.auth_uri = settingsObject["auth_uri"].toString(); | |
id.client_id = settingsObject["client_id"].toString(); | |
id.token_uri = settingsObject["token_uri"].toString(); | |
id.client_secret = settingsObject["client_secret"].toString(); | |
return id; | |
} | |
YouTube::YouTube(QObject* parent) : QObject(parent) { | |
} | |
YouTube::~YouTube() { | |
} | |
QOAuth2AuthorizationCodeFlow* YouTube::InitOAuth(ClientID client, /*out,async*/QString & access_token) const { | |
// ensure that OpenSSL DLLs are found | |
// download from https://slproweb.com/products/Win32OpenSSL.html if missing | |
assert(QSslSocket::supportsSsl()); | |
// https://developers.google.com/identity/protocols/oauth2/scopes#youtube | |
auto* auth = new QOAuth2AuthorizationCodeFlow(const_cast<YouTube*>(this)); | |
auth->setScope(s_scope); | |
auth->setAuthorizationUrl(client.auth_uri); | |
auth->setClientIdentifier(client.client_id); | |
auth->setAccessTokenUrl(client.token_uri); | |
auth->setClientIdentifierSharedKey(client.client_secret); | |
// setup local web server to receive access_token | |
auto* replyHandler = new QOAuthHttpServerReplyHandler(8080, auth); | |
auth->setReplyHandler(replyHandler); | |
connect(replyHandler, &QOAuthHttpServerReplyHandler::tokensReceived, [&](const QVariantMap& map) { | |
access_token = map["access_token"].toString(); // deferred write | |
}); | |
// open in default web browser | |
connect(auth, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &QDesktopServices::openUrl); | |
connect(auth, &QOAuth2AuthorizationCodeFlow::granted, this, &YouTube::VideoUploadRequest); | |
return auth; | |
} | |
void YouTube::UploadFile(QUrl filename) { | |
m_video_file_content = YouTube::LoadFile(filename.toLocalFile()); | |
if (m_limited_auth) { | |
// authentication from limited input-device | |
LimitedInputAuthRequest(); | |
} else { | |
// regular on-device OAuth authentication | |
QOAuth2AuthorizationCodeFlow* auth = InitOAuth(ParseClientID(m_client_file), m_access_token); | |
auth->grant(); | |
} | |
} | |
void YouTube::LimitedInputAuthRequest() { | |
ClientID client_id = ParseClientID(m_client_file); | |
QNetworkRequest request; | |
{ | |
QUrl url("https://oauth2.googleapis.com/device/code"); | |
QUrlQuery query; | |
query.addQueryItem("client_id", client_id.client_id); | |
query.addQueryItem("scope", s_scope); | |
url.setQuery(query); | |
request = QNetworkRequest(url); | |
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); | |
} | |
QByteArray body; // empty | |
QNetworkAccessManager* auth = new QNetworkAccessManager(this); | |
connect(auth, &QNetworkAccessManager::finished, this, &YouTube::LimitedInputAuthResponse); | |
auth->post(request, body); | |
} | |
void YouTube::LimitedInputAuthResponse(QNetworkReply* reply) { | |
auto error = reply->error(); | |
QByteArray body = reply->readAll(); | |
if (error != QNetworkReply::NoError) { | |
Q_EMIT failed(body); | |
return; | |
} | |
QJsonObject object = QJsonDocument::fromJson(body).object(); | |
m_device_code = object["device_code"].toString(); | |
QString user_code = object["user_code"].toString(); // max 15 chars long | |
QString verification_url = object["verification_url"].toString(); // max 40 chars long | |
//int expires_in = object["expires_in"].toInt(); | |
m_poll_interval = object["interval"].toInt(); // time [sec] between polling requests | |
// display auth instructions to end-user | |
emit showLimitedAuthURL(verification_url, user_code); | |
// defered polling of authentication result | |
QTimer::singleShot(1000 * m_poll_interval, this, &YouTube::LimitedInputResultRequest); | |
return; | |
} | |
void YouTube::LimitedInputResultRequest() { | |
// poll authentication result | |
ClientID client_id = ParseClientID(m_client_file); | |
QNetworkRequest request; | |
{ | |
QUrl url("https://oauth2.googleapis.com/token"); | |
QUrlQuery query; | |
query.addQueryItem("client_id", client_id.client_id); | |
query.addQueryItem("client_secret", client_id.client_secret); | |
query.addQueryItem("device_code", m_device_code); | |
query.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); | |
url.setQuery(query); | |
request = QNetworkRequest(url); | |
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); | |
} | |
QByteArray body; // empty | |
QNetworkAccessManager* auth = new QNetworkAccessManager(this); | |
connect(auth, &QNetworkAccessManager::finished, this, &YouTube::LimitedInputResultResponse); | |
auth->post(request, body); | |
} | |
void YouTube::LimitedInputResultResponse(QNetworkReply* reply) { | |
auto error = reply->error(); | |
QByteArray body = reply->readAll(); | |
QJsonObject object = QJsonDocument::fromJson(body).object(); | |
if (error == QNetworkReply::UnknownContentError) { | |
// retry polling of authentication result | |
QTimer::singleShot(1000 * m_poll_interval, this, &YouTube::LimitedInputResultRequest); | |
return; | |
} else if (error != QNetworkReply::NoError) { | |
QString error = object["error"].toString(); | |
Q_EMIT failed(error); | |
return; | |
} | |
// authentication suceeded | |
m_access_token = object["access_token"].toString(); | |
int expires_in = object["expires_in"].toInt(); | |
QString scope = object["scope"].toString(); | |
QString token_type = object["token_type"].toString(); | |
QString refresh_token = object["refresh_token"].toString(); | |
VideoUploadRequest(); | |
} | |
void YouTube::VideoUploadRequest() { | |
QNetworkRequest request; | |
{ | |
assert(m_api_file.length() > 0); | |
assert(m_access_token.length() > 0); | |
QString api_key = LoadFile(m_api_file); | |
QUrl url("https://www.googleapis.com/upload/youtube/v3/videos"); | |
QUrlQuery query; | |
query.addQueryItem("part", "snippet, status"); | |
query.addQueryItem("key", api_key); | |
query.addQueryItem("uploadType", "resumable"); | |
url.setQuery(query); | |
request = QNetworkRequest(url); | |
request.setHeader(QNetworkRequest::ContentTypeHeader, "video/*"); | |
request.setRawHeader("Authorization", ("Bearer " + m_access_token).toUtf8()); | |
} | |
QByteArray body; | |
{ | |
QJsonArray tags; | |
for (auto tag : m_tags) | |
tags.push_back(QJsonValue::fromVariant(tag)); | |
QJsonObject snippet; | |
snippet.insert("title", QJsonValue::fromVariant(m_title)); | |
snippet.insert("description", QJsonValue::fromVariant(m_description)); | |
snippet.insert("tags", tags); | |
snippet.insert("categoryId", QJsonValue::fromVariant(m_category)); | |
QJsonObject status; | |
status.insert("privacyStatus", "unlisted"); | |
QJsonObject json_arr; | |
json_arr.insert("snippet", snippet); | |
json_arr.insert("status", status); | |
QJsonDocument doc(json_arr); | |
body = doc.toJson(); | |
} | |
QNetworkAccessManager* downloader = new QNetworkAccessManager(this); | |
connect(downloader, &QNetworkAccessManager::finished, this, &YouTube::VideoUploadData); | |
downloader->post(request, body); | |
} | |
void YouTube::VideoUploadData(QNetworkReply* reply) { | |
auto error = reply->error(); | |
if (error != QNetworkReply::NoError) { | |
QByteArray body = reply->readAll(); | |
QJsonObject object = QJsonDocument::fromJson(body).object(); | |
QString error = object["error"].toObject()["message"].toString(); | |
Q_EMIT failed(error); | |
return; | |
} | |
QString upload_url = reply->rawHeader("Location"); | |
QNetworkRequest request(upload_url); | |
QNetworkAccessManager * uploader = new QNetworkAccessManager(this); | |
connect(uploader, &QNetworkAccessManager::finished, this, &YouTube::VideoUploadResult); | |
uploader->put(request, m_video_file_content); | |
} | |
void YouTube::VideoUploadResult(QNetworkReply* reply) { | |
QByteArray body = reply->readAll(); | |
QJsonObject object = QJsonDocument::fromJson(body).object(); | |
auto upload_error = reply->error(); | |
if (upload_error != QNetworkReply::NoError) { | |
QString error = object["error"].toObject()["message"].toString(); | |
Q_EMIT failed(error); | |
} else { | |
QString id = object["id"].toString(); | |
QUrl url("https://youtu.be/" + id); | |
Q_EMIT succeeded(url); | |
// open video in default web browser | |
QDesktopServices::openUrl(url); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment