From 46052605acacf1fd34f6ae987017356c2638fabe Mon Sep 17 00:00:00 2001
From: Daniel Karbach <daniel.karbach@localhorst.tv>
Date: Wed, 26 Feb 2025 18:47:44 +0100
Subject: [PATCH] new clips api

---
 src/app/ClipPlayer.cpp    | 15 +++++++++++++-
 src/app/ClipPlayer.h      |  7 +++----
 src/twitch/Clip.h         | 41 +++++++++++++++++++++++++++++++++------
 src/ws/Connection.cpp     | 36 ++++++++++++++++++++++++++++++++++
 src/ws/HttpsConnection.h  | 14 ++++++++-----
 src/ws/TwitchConnection.h |  2 ++
 6 files changed, 99 insertions(+), 16 deletions(-)

diff --git a/src/app/ClipPlayer.cpp b/src/app/ClipPlayer.cpp
index ea840b8..fce1d92 100644
--- a/src/app/ClipPlayer.cpp
+++ b/src/app/ClipPlayer.cpp
@@ -7,13 +7,17 @@
 
 namespace app {
 
-void ClipPlayer::LoadRandomClip(const Json::Value &json) {
+void ClipPlayer::LoadRandomClip(const Json::Value &json, ws::TwitchConnection &twitch) {
 	const Json::Value &data = json["data"];
 	if (!data.isArray()) {
+		loading = false;
+		done = true;
 		state.QueueIRCMessage("Ungültiges Datenformat in der Twitch API Response DansGame");
 		return;
 	}
 	if (data.empty()) {
+		loading = false;
+		done = true;
 		state.QueueIRCMessage("Channel " + channel.title + " hat keine Clips NotLikeThis");
 		return;
 	}
@@ -22,11 +26,20 @@ void ClipPlayer::LoadRandomClip(const Json::Value &json) {
 	Json::ArrayIndex choice = dist(state.GetRNG());
 	const Json::Value &clip_json = data[choice];
 	clip = twitch::Clip(clip_json);
+	clip.FetchVideoURL(twitch)
+		.Then([this](const twitch::Clip *clip) -> void {
+			loading = false;
+		})
+		.Catch([this](ws::HttpsConnection *rsp) -> void {
+			loading = false;
+			done = true;
+		});
 }
 
 void ClipPlayer::Start(const Clock &clock) {
 	start_time = clock;
 	running = true;
+
 	if (clip.HasVideo()) {
 		std::cout << "adding clip " << clip.GetVideoURL() << " at " << clock << std::endl;
 		Media &media = state.AddMedia(clip.GetVideoURL().c_str());
diff --git a/src/app/ClipPlayer.h b/src/app/ClipPlayer.h
index 6eb9d4b..78f4619 100644
--- a/src/app/ClipPlayer.h
+++ b/src/app/ClipPlayer.h
@@ -68,9 +68,8 @@ public:
 	void FetchClip(ws::TwitchConnection &twitch) {
 		loading = true;
 		twitch.FetchClips(channel.twitch_id)
-			.Then([this](const Json::Value *rsp) -> void {
-				loading = false;
-				LoadRandomClip(*rsp);
+			.Then([this, &twitch](const Json::Value *rsp) -> void {
+				LoadRandomClip(*rsp, twitch);
 			})
 			.Catch([this](ws::HttpsConnection *rsp) -> void {
 				loading = false;
@@ -80,7 +79,7 @@ public:
 			});
 	}
 
-	void LoadRandomClip(const Json::Value &json);
+	void LoadRandomClip(const Json::Value &json, ws::TwitchConnection &twitch);
 
 	void Start(const Clock &clock);
 
diff --git a/src/twitch/Clip.h b/src/twitch/Clip.h
index 3e47db4..0dc2bf3 100644
--- a/src/twitch/Clip.h
+++ b/src/twitch/Clip.h
@@ -1,36 +1,64 @@
 #ifndef TEST_TWITCH_CLIP_H_
 #define TEST_TWITCH_CLIP_H_
 
+#include "json/value.h"
 #include <iostream>
 #include <json/json.h>
 
+#include "../sys/Promise.h"
+#include "../ws/HttpsConnection.h"
+#include "../ws/TwitchConnection.h"
+
 namespace twitch {
 
 class Clip {
 
+public:
+	typedef sys::Promise<const Clip *, ws::HttpsConnection *> Promise;
+
 public:
 	Clip() {
 	}
 	explicit Clip(const Json::Value &json)
-	: broadcaster_name(json["broadcaster_name"].asString())
+	: id(json["id"].asString())
+	, broadcaster_name(json["broadcaster_name"].asString())
 	, creator_name(json["creator_name"].asString())
 	, thumbnail_url(json["thumbnail_url"].asString())
 	, title(json["title"].asString())
 	, duration(json["duration"].asDouble())
 	, url(json["url"].asString()) {
 		std::cout << "clip: " << json << std::endl;
-		size_t thumb_pos = thumbnail_url.find("-preview-");
-		if (thumb_pos != std::string::npos) {
-			video_url = thumbnail_url.substr(0, thumb_pos);
-			video_url += ".mp4";
-		}
 	}
 
 public:
+	Promise FetchVideoURL(ws::TwitchConnection &twitch) {
+		Promise promise;
+		twitch.GetClipAccessToken(id)
+			.Then([=](const Json::Value *json) mutable -> void {
+				Json::Value data = (*json)[0]["data"]["clip"];
+				video_url = data["videoQualities"][0]["sourceURL"].asString();
+				video_url.append("?sig=");
+				ws::HttpsConnection::UrlEncode(data["playbackAccessToken"]["signature"].asString(), video_url);
+				video_url.append("&token=");
+				ws::HttpsConnection::UrlEncode(data["playbackAccessToken"]["value"].asString(), video_url);
+				promise.Resolve(this);
+			})
+			.Catch([=](ws::HttpsConnection *rsp) mutable -> void {
+				std::cout << "error requesting access token" << std::endl;
+				std::cout << rsp->GetBody() << std::endl;
+				promise.Reject(rsp);
+			});
+		return promise;
+	}
+
 	bool HasVideo() const {
 		return !video_url.empty();
 	}
 
+	const std::string &GetID() const {
+		return id;
+	}
+
 	const std::string &GetBroadcasterName() const {
 		return broadcaster_name;
 	}
@@ -52,6 +80,7 @@ public:
 	}
 
 private:
+	std::string id;
 	std::string broadcaster_name;
 	std::string creator_name;
 	std::string thumbnail_url;
diff --git a/src/ws/Connection.cpp b/src/ws/Connection.cpp
index 12dced7..5580319 100644
--- a/src/ws/Connection.cpp
+++ b/src/ws/Connection.cpp
@@ -3,6 +3,7 @@
 #include "PusherConnection.h"
 #include "TwitchConnection.h"
 
+#include "json/value.h"
 #include <cstdio>
 #include <iostream>
 #include <json/json.h>
@@ -287,6 +288,39 @@ TwitchConnection::WebPromise TwitchConnection::FetchClips(const std::string &fro
 			.Catch([=](HttpsConnection *rsp) mutable -> void {
 				promise.Reject(rsp);
 			});
+	}).Catch([=](HttpsConnection *rsp) mutable -> void {
+		promise.Reject(rsp);
+	});
+	return promise;
+}
+
+TwitchConnection::WebPromise TwitchConnection::GetClipAccessToken(const std::string &slug) {
+	WebPromise promise;
+	AuthorizedRequest("POST", "gql.twitch.tv", "/gql").Then([=](HttpsConnection *req) -> void {
+		req->SetHeader("Content-Type", "text/plain; charset=UTF-8");
+		req->SetHeader("Client-Id", "kimne78kx3ncx6brgo4mv6wki5h1ko");
+		Json::Value json(Json::arrayValue);
+		json[0]["extensions"]["persistedQuery"]["version"] = 1;
+		json[0]["extensions"]["persistedQuery"]["sha256Hash"] = "6fd3af2b22989506269b9ac02dd87eb4a6688392d67d94e41a6886f1e9f5c00f";
+		json[0]["operationName"] = "VideoAccessToken_Clip";
+		json[0]["variables"]["platform"] = "web";
+		json[0]["variables"]["slug"] = slug;
+		req->AddBody(json.toStyledString());
+		req->SetContentLength();
+		req->GetPromise()
+			.Then([this, promise](HttpsConnection *rsp) mutable -> void {
+				if (rsp->IsPositive()) {
+					Json::Value json = rsp->GetBodyJSON();
+					promise.Resolve(&json);
+				} else {
+					promise.Reject(rsp);
+				}
+			})
+			.Catch([=](HttpsConnection *rsp) mutable -> void {
+				promise.Reject(rsp);
+			});
+	}).Catch([=](HttpsConnection *rsp) mutable -> void {
+		promise.Reject(rsp);
 	});
 	return promise;
 }
@@ -311,6 +345,8 @@ TwitchConnection::WebPromise TwitchConnection::Shoutout(const std::string &from,
 			.Catch([this, promise](HttpsConnection *rsp) mutable -> void {
 				promise.Reject(rsp);
 			});
+	}).Catch([=](HttpsConnection *rsp) mutable -> void {
+		promise.Reject(rsp);
 	});
 	return promise;
 }
diff --git a/src/ws/HttpsConnection.h b/src/ws/HttpsConnection.h
index 2e5f617..403e369 100644
--- a/src/ws/HttpsConnection.h
+++ b/src/ws/HttpsConnection.h
@@ -50,15 +50,19 @@ public:
 	}
 
 	void AddFormUrlencPart(const std::string &s) {
+		UrlEncode(s, out_buffer);
+	}
+
+	static void UrlEncode(const std::string &s, std::string &out) {
 		for (const char c : s) {
 			if (c == ' ') {
-				out_buffer.push_back('+');
+				out.push_back('+');
 			} else if (c < 32 || c > 127 || c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@' || c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || c == '=' || c == '%') {
-				out_buffer.push_back('%');
-				out_buffer.push_back(HexDigit(c / 16));
-				out_buffer.push_back(HexDigit(c % 16));
+				out.push_back('%');
+				out.push_back(HexDigit(c / 16));
+				out.push_back(HexDigit(c % 16));
 			} else {
-				out_buffer.push_back(c);
+				out.push_back(c);
 			}
 		}
 	}
diff --git a/src/ws/TwitchConnection.h b/src/ws/TwitchConnection.h
index 3b430b9..b02e4a6 100644
--- a/src/ws/TwitchConnection.h
+++ b/src/ws/TwitchConnection.h
@@ -105,6 +105,8 @@ public:
 
 	WebPromise FetchClips(const std::string &from);
 
+	WebPromise GetClipAccessToken(const std::string &slug);
+
 	WebPromise Shoutout(const std::string &from, const std::string &to);
 
 public:
-- 
2.39.5