Skip to content

Instantly share code, notes, and snippets.

@forderud
Last active February 15, 2024 16:40
Show Gist options
  • Save forderud/941e1d2014c43a0e3f5f0af1d7972075 to your computer and use it in GitHub Desktop.
Save forderud/941e1d2014c43a0e3f5f0af1d7972075 to your computer and use it in GitHub Desktop.
YouTube video upload from Qt/C++
#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