Compare commits

...

3 commits

Author SHA1 Message Date
ce634d128a
add videoplayback rx bytes metrics
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m8s
2025-04-05 03:47:31 -03:00
2bbe02b69d
rebase 2025-04-05 03:47:24 -03:00
b3dca7a660
update patches and invidious-companion 2025-04-05 03:44:27 -03:00
15 changed files with 116 additions and 3439 deletions

@ -1 +1 @@
Subproject commit ce3ba082d2d04fb85c74004835287f56faccb41a
Subproject commit ca08ef176acf6af7b6339670335a329db8196634

View file

@ -1,7 +1,7 @@
From ad4b5aca25433218a7e903acffece4ebf222127e Mon Sep 17 00:00:00 2001
From 16ba943bf29f1745273eaeea29993c6eb710f287 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 19:37:34 -0300
Subject: [PATCH 01/13] ci: update deno to 2.2.5
Subject: [PATCH 01/12] ci: update deno to 2.2.5
---
Dockerfile | 2 +-

View file

@ -1,7 +1,7 @@
From 38a460ca36d462c7d4396a4a42266c318d56c505 Mon Sep 17 00:00:00 2001
From f335b46fafac4babfd1a267d5a719c606d89c890 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 18:44:10 -0300
Subject: [PATCH 02/13] feat: add support for an external videoplayback proxy
Subject: [PATCH 02/12] feat: add support for an external videoplayback proxy
---
config/config.example.toml | 1 +
@ -11,10 +11,10 @@ Subject: [PATCH 02/13] feat: add support for an external videoplayback proxy
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/config/config.example.toml b/config/config.example.toml
index 5989656..0b4d7b8 100644
index cce4704..4aec8e9 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -27,6 +27,7 @@
@@ -28,6 +28,7 @@
# #proxy = "" # env variable: PROXY
# # Enable YouTube new video format UMP
# ump = false
@ -23,10 +23,10 @@ index 5989656..0b4d7b8 100644
###
# Network call timeouts when talking to YouTube.
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index 426426f..273fe95 100644
index cd851d7..a233f83 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -29,6 +29,9 @@ const ConfigSchema = z.object({
@@ -32,6 +32,9 @@ export const ConfigSchema = z.object({
debounce_multiplier: z.number().optional(),
}).strict().optional(),
}).strict().optional(),
@ -37,10 +37,10 @@ index 426426f..273fe95 100644
jobs: z.object({
youtube_session: z.object({
diff --git a/src/routes/invidious_routes/dashManifest.ts b/src/routes/invidious_routes/dashManifest.ts
index 3834601..d69f1cf 100644
index d241407..b4446b6 100644
--- a/src/routes/invidious_routes/dashManifest.ts
+++ b/src/routes/invidious_routes/dashManifest.ts
@@ -91,8 +91,10 @@ dashManifest.get("/:videoId", async (c) => {
@@ -93,8 +93,10 @@ dashManifest.get("/:videoId", async (c) => {
queryParams.set("enc", "true");
queryParams.set("data", encryptedParams);
}
@ -54,10 +54,10 @@ index 3834601..d69f1cf 100644
} else {
return dashUrl;
diff --git a/src/routes/invidious_routes/latestVersion.ts b/src/routes/invidious_routes/latestVersion.ts
index 6421904..07b070a 100644
index 331be18..74dd090 100644
--- a/src/routes/invidious_routes/latestVersion.ts
+++ b/src/routes/invidious_routes/latestVersion.ts
@@ -83,7 +83,8 @@ latestVersion.get("/", async (c) => {
@@ -85,7 +85,8 @@ latestVersion.get("/", async (c) => {
queryParams.set("enc", "true");
queryParams.set("data", encryptedParams);
}

View file

@ -1,7 +1,7 @@
From ca369efa1837eff09e782f2e2d363ee23d66a43e Mon Sep 17 00:00:00 2001
From ada8d2b388d68abe6c5e54eb8f86214bb4a92e22 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 18:52:53 -0300
Subject: [PATCH 03/13] feat: report the external videoplayback proxy via /info
Subject: [PATCH 03/12] feat: report the external videoplayback proxy via /info
endpoint
---
@ -11,21 +11,21 @@ Subject: [PATCH 03/13] feat: report the external videoplayback proxy via /info
create mode 100644 src/routes/info.ts
diff --git a/src/routes/index.ts b/src/routes/index.ts
index e67b618..6448e3d 100644
index 5aa9fa1..fde6a15 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -10,6 +10,7 @@ import getDownloadHandler from "./invidious_routes/download.ts";
import videoPlaybackProxy from "./videoPlaybackProxy.ts";
@@ -11,6 +11,7 @@ import videoPlaybackProxy from "./videoPlaybackProxy.ts";
import health from "./health.ts";
import type { Config } from "../lib/helpers/config.ts";
import metrics from "./metrics.ts";
+import info from "./info.ts";
export const routes = (
app: Hono,
@@ -32,4 +33,5 @@ export const routes = (
app.route("/api/v1/captions", invidiousCaptionsApi);
app.route("/videoplayback", videoPlaybackProxy);
app.route("/healthz", health);
@@ -36,4 +37,5 @@ export const routes = (
if (config.server.enable_metrics) {
app.route("/metrics", metrics);
}
+ app.route("/info", info);
};
diff --git a/src/routes/info.ts b/src/routes/info.ts

View file

@ -1,7 +1,7 @@
From cd276db4eee8885f0022dc5a72ea069de8b54fd1 Mon Sep 17 00:00:00 2001
From d09d9d37f9ed91dea72e4520fa61222ebc96fd9b Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 19:02:01 -0300
Subject: [PATCH 04/13] feat: add resolution limit on DASH streams to save
Subject: [PATCH 04/12] feat: add resolution limit on DASH streams to save
bandwidth
---
@ -11,24 +11,24 @@ Subject: [PATCH 04/13] feat: add resolution limit on DASH streams to save
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/config/config.example.toml b/config/config.example.toml
index 0b4d7b8..6d342bb 100644
index 4aec8e9..8e2af1c 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -16,6 +16,7 @@
# secret_key = "CHANGE_ME" # env variable: SERVER_SECRET_KEY
@@ -17,6 +17,7 @@
# verify_requests = false
# encrypt_query_params = false # env variable: SERVER_ENCRYPT_QUERY_PARAMS
# enable_metrics = false # env variable: SERVER_ENABLE_METRICS
+# max_dash_resolution = 1080
# [cache]
# enabled = true
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index 273fe95..d1968fe 100644
index a233f83..e559271 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -12,6 +12,9 @@ const ConfigSchema = z.object({
encrypt_query_params: z.boolean().default(
Deno.env.get("SERVER_ENCRYPT_QUERY_PARAMS") === "true" || false,
@@ -15,6 +15,9 @@ export const ConfigSchema = z.object({
enable_metrics: z.boolean().default(
Deno.env.get("SERVER_ENABLE_METRICS") === "true" || false,
),
+ max_dash_resolution: z.number().default(
+ Number(Deno.env.get("SERVER_MAX_DASH_RESOLUTION")),
@ -37,10 +37,10 @@ index 273fe95..d1968fe 100644
cache: z.object({
enabled: z.boolean().default(true),
diff --git a/src/routes/invidious_routes/dashManifest.ts b/src/routes/invidious_routes/dashManifest.ts
index d69f1cf..10b23d8 100644
index b4446b6..a691d6e 100644
--- a/src/routes/invidious_routes/dashManifest.ts
+++ b/src/routes/invidious_routes/dashManifest.ts
@@ -53,7 +53,8 @@ dashManifest.get("/:videoId", async (c) => {
@@ -55,7 +55,8 @@ dashManifest.get("/:videoId", async (c) => {
videoInfo.streaming_data.adaptive_formats = videoInfo
.streaming_data.adaptive_formats
.filter((i) =>

View file

@ -1,17 +1,17 @@
From 15a903e7478366173e4ac90f1eb96526fc94df8d Mon Sep 17 00:00:00 2001
From 1e1584338b7b023db0196062a998bd0e6ca9a394 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 19:06:04 -0300
Subject: [PATCH 05/13] feat: add env variable to set verify_requests
Subject: [PATCH 05/12] feat: add env variable to set verify_requests
---
src/lib/helpers/config.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index d1968fe..a5221c9 100644
index e559271..cd0489d 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -8,7 +8,9 @@ const ConfigSchema = z.object({
@@ -8,7 +8,9 @@ export const ConfigSchema = z.object({
secret_key: z.string().length(16).default(
Deno.env.get("SERVER_SECRET_KEY") || "",
),

View file

@ -1,7 +1,7 @@
From d2fd6b4cc2aa123b82ba0c11a5d4593ca8cdc4dc Mon Sep 17 00:00:00 2001
From de0779fbbb394ab5378490194148dea4692f80ee Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 19:20:52 -0300
Subject: [PATCH 06/13] feat: add support for multiple proxies
Subject: [PATCH 06/12] feat: add support for multiple proxies
---
src/lib/helpers/getFetchClient.ts | 17 ++++++++++++++++-

View file

@ -1,7 +1,7 @@
From 600734d4b7731ad70605770cf99060d5af382647 Mon Sep 17 00:00:00 2001
From cfa953c99a59d38c5b3a0a540da75a5144c3469d Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Mon, 24 Mar 2025 20:34:33 -0300
Subject: [PATCH 07/13] feat: add option to disable potoken generation check
Subject: [PATCH 07/12] feat: add option to disable potoken generation check
---
config/config.example.toml | 1 +
@ -10,10 +10,10 @@ Subject: [PATCH 07/13] feat: add option to disable potoken generation check
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/config/config.example.toml b/config/config.example.toml
index 6d342bb..969c768 100644
index 8e2af1c..b1cb4ee 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -52,6 +52,7 @@
@@ -53,6 +53,7 @@
# [jobs.youtube_session]
# po_token_enabled = true # whether to generate PO tokens
# frequency = "*/5 * * * *" # frequency of PO token refresh in cron format
@ -22,10 +22,10 @@ index 6d342bb..969c768 100644
# [youtube_session]
# oauth_enabled = false
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index a5221c9..e6b1070 100644
index cd0489d..6004753 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -42,6 +42,15 @@ const ConfigSchema = z.object({
@@ -45,6 +45,15 @@ export const ConfigSchema = z.object({
youtube_session: z.object({
po_token_enabled: z.boolean().default(true),
frequency: z.string().default("*/5 * * * *"),
@ -42,10 +42,10 @@ index a5221c9..e6b1070 100644
}).strict().default({}),
youtube_session: z.object({
diff --git a/src/lib/jobs/potoken.ts b/src/lib/jobs/potoken.ts
index 6867082..3e5dfc0 100644
index a628996..8ec2034 100644
--- a/src/lib/jobs/potoken.ts
+++ b/src/lib/jobs/potoken.ts
@@ -166,7 +166,9 @@ export const poTokenGenerate = async (
@@ -177,7 +177,9 @@ async function checkToken({
"failed to find valid video with adaptive format to check token against",
);
}

View file

@ -1,7 +1,7 @@
From a7672c1dba33fceb9c4de722a2ca6b2f56767007 Mon Sep 17 00:00:00 2001
From ad3280baeed89bc68a655469dd5db10161d2421a Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Tue, 25 Mar 2025 00:04:47 -0300
Subject: [PATCH 08/13] add proxy retries on innertube error
Subject: [PATCH 08/12] add proxy retries on innertube error
---
src/lib/helpers/config.ts | 1 +
@ -9,10 +9,10 @@ Subject: [PATCH 08/13] add proxy retries on innertube error
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index e6b1070..acbede2 100644
index 6004753..c840ce4 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -37,6 +37,7 @@ const ConfigSchema = z.object({
@@ -40,6 +40,7 @@ export const ConfigSchema = z.object({
external_videoplayback_proxy: z.string().default(
Deno.env.get("EXTERNAL_VIDEOPLAYBACK_PROXY") || "",
),
@ -21,10 +21,10 @@ index e6b1070..acbede2 100644
jobs: z.object({
youtube_session: z.object({
diff --git a/src/lib/helpers/youtubePlayerHandling.ts b/src/lib/helpers/youtubePlayerHandling.ts
index c7c2f74..396eabf 100644
index 4c9ab51..792ba54 100644
--- a/src/lib/helpers/youtubePlayerHandling.ts
+++ b/src/lib/helpers/youtubePlayerHandling.ts
@@ -40,12 +40,33 @@ export const youtubePlayerParsing = async ({
@@ -43,12 +43,33 @@ export const youtubePlayerParsing = async ({
if (videoCached != null && cacheEnabled) {
return JSON.parse(new TextDecoder().decode(decompress(videoCached)));
} else {

View file

@ -1,7 +1,7 @@
From dc1df7a313083757fd5b74da3dc1739b0e45eb27 Mon Sep 17 00:00:00 2001
From b33b8ee6f6ac106dc1515ad610df61c23051fe7e Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Tue, 25 Mar 2025 00:07:28 -0300
Subject: [PATCH 10/13] add metrics for proxy retries
Subject: [PATCH 09/12] add metrics for proxy retries
---
src/lib/helpers/metrics.ts | 5 +++++
@ -9,10 +9,10 @@ Subject: [PATCH 10/13] add metrics for proxy retries
2 files changed, 6 insertions(+)
diff --git a/src/lib/helpers/metrics.ts b/src/lib/helpers/metrics.ts
index 98ca83d..93c6b82 100644
index ca972df..5dee540 100644
--- a/src/lib/helpers/metrics.ts
+++ b/src/lib/helpers/metrics.ts
@@ -55,6 +55,11 @@ export class Metrics {
@@ -58,6 +58,11 @@ export class Metrics {
"Number failed requests made to the Innertube API for whatever reason",
);
@ -21,11 +21,11 @@ index 98ca83d..93c6b82 100644
+ 'Times a request to innertube has been retried when it gets "This helps protect our community"',
+ );
+
public checkInnertubeResponse(videoData: IRawResponse) {
this.innertubeFailedRequest.inc();
private checkStatus(videoData: IRawResponse) {
const status = videoData.playabilityStatus?.status;
diff --git a/src/lib/helpers/youtubePlayerHandling.ts b/src/lib/helpers/youtubePlayerHandling.ts
index 2ae878d..7b5f0c1 100644
index 792ba54..9efed59 100644
--- a/src/lib/helpers/youtubePlayerHandling.ts
+++ b/src/lib/helpers/youtubePlayerHandling.ts
@@ -62,6 +62,7 @@ export const youtubePlayerParsing = async ({

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
From a9cc6d6dc6953ec1ea5bd49cb80e78ed25b6e0af Mon Sep 17 00:00:00 2001
From 702ebebb73b60e0afa8d3658b20fabcbc8515496 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Tue, 25 Mar 2025 00:24:07 -0300
Subject: [PATCH 11/13] fix: fix tokio overflow on compile
Subject: [PATCH 10/12] fix: fix tokio overflow on compile
---
Dockerfile | 2 ++

View file

@ -1,17 +1,17 @@
From 046aa5c93e998ceaba089ab38b6ee5ed7162705c Mon Sep 17 00:00:00 2001
From 6d9b72e9a8c8bc56932add51db801f7801396ca3 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Wed, 26 Mar 2025 12:24:49 -0300
Subject: [PATCH 12/13] Add environment variable for youtube_session.frequency
Subject: [PATCH 11/12] Add environment variable for youtube_session.frequency
---
src/lib/helpers/config.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index 50f3f8b..8cf34fd 100644
index c840ce4..6b9170d 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -51,7 +51,9 @@ const ConfigSchema = z.object({
@@ -45,7 +45,9 @@ export const ConfigSchema = z.object({
jobs: z.object({
youtube_session: z.object({
po_token_enabled: z.boolean().default(true),

View file

@ -0,0 +1,50 @@
From 01e545dc92027614ba3a9c514fa4f2a3c1c0fa9d Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Sat, 5 Apr 2025 03:42:58 -0300
Subject: [PATCH 12/12] add videoplayback rx bytes metrics
---
src/lib/helpers/metrics.ts | 5 +++++
src/routes/videoPlaybackProxy.ts | 3 +++
2 files changed, 8 insertions(+)
diff --git a/src/lib/helpers/metrics.ts b/src/lib/helpers/metrics.ts
index 5dee540..27f84c3 100644
--- a/src/lib/helpers/metrics.ts
+++ b/src/lib/helpers/metrics.ts
@@ -63,6 +63,11 @@ export class Metrics {
'Times a request to innertube has been retried when it gets "This helps protect our community"',
);
+ public videoplaybackRxBytes = this.createCounter(
+ "videoplayback_rx_bytes_count",
+ 'How many bytes have been received from google servers',
+ );
+
private checkStatus(videoData: IRawResponse) {
const status = videoData.playabilityStatus?.status;
diff --git a/src/routes/videoPlaybackProxy.ts b/src/routes/videoPlaybackProxy.ts
index b6cce87..74d5324 100644
--- a/src/routes/videoPlaybackProxy.ts
+++ b/src/routes/videoPlaybackProxy.ts
@@ -35,6 +35,7 @@ videoPlaybackProxy.get("/", async (c) => {
const urlReq = new URL(c.req.url);
const config = c.get("config");
const queryParams = new URLSearchParams(urlReq.search);
+ const metrics = c.get("metrics");
if (c.req.query("enc") === "true") {
const { data: encryptedQuery } = c.req.query();
@@ -180,6 +181,8 @@ videoPlaybackProxy.get("/", async (c) => {
}
}
+ metrics?.videoplaybackRxBytes.inc(Number(googlevideoResponse.headers.get("content-length")))
+
return new Response(googlevideoResponse.body, {
status: responseStatus,
statusText: googlevideoResponse.statusText,
--
2.49.0

View file

@ -1,699 +0,0 @@
From 9d4ad948d08584a771995bf06ddf209fc8c97ee7 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>
Date: Tue, 1 Apr 2025 19:18:15 -0300
Subject: [PATCH 13/13] Move po token to webworker
https://github.com/iv-org/invidious-companion/pull/73
Squashed commit of the following:
commit 99929b0e8d1e26ca872437e0f1799efa3632dc28
Author: Alex Maras <dev@alexmaras.com>
Date: Wed Mar 26 21:43:25 2025 +0800
chore: remove memory logging
commit 12f7eee268cbc4224b6e2778d7e3abbbac9ae011
Author: Alex Maras <dev@alexmaras.com>
Date: Mon Mar 24 18:14:40 2025 +0800
chore: deno fmt
commit 9befe1b2bf6d2294b1765213bb5be98d649cce06
Author: Alex Maras <dev@alexmaras.com>
Date: Mon Mar 24 18:13:25 2025 +0800
chore: improve typing in worker.ts
commit 186efd73c05980b91c8e81290327cf5f8eaa1629
Author: Alex Maras <dev@alexmaras.com>
Date: Sat Mar 22 10:57:43 2025 +0800
chore: fmt
commit 391f91571a212d46055e35edfaa00b8efc2b5bc1
Author: Alex Maras <dev@alexmaras.com>
Date: Sat Mar 22 10:57:08 2025 +0800
chore: use z.union instead of .or in worker
commit 3df53068293b513b6502f0bbb29d549027411c5a
Author: Alex Maras <dev@alexmaras.com>
Date: Thu Mar 20 14:06:52 2025 +0800
chore: temporary heap memory usage output
commit a1cb9da1ca3cd8890007d9b2ff4b2b400860228a
Author: Alex Maras <dev@alexmaras.com>
Date: Thu Mar 20 14:06:24 2025 +0800
feat: split po token generation to web worker
---
src/lib/helpers/config.ts | 2 +-
src/lib/helpers/youtubePlayerHandling.ts | 4 +-
src/lib/helpers/youtubePlayerReq.ts | 6 +-
src/lib/jobs/potoken.ts | 225 +++++++++++-----------
src/lib/jobs/worker.ts | 231 +++++++++++++++++++++++
src/lib/types/HonoVariables.ts | 4 +-
src/main.ts | 22 +--
7 files changed, 361 insertions(+), 133 deletions(-)
create mode 100644 src/lib/jobs/worker.ts
diff --git a/src/lib/helpers/config.ts b/src/lib/helpers/config.ts
index 8cf34fd..2a3fdb3 100644
--- a/src/lib/helpers/config.ts
+++ b/src/lib/helpers/config.ts
@@ -1,7 +1,7 @@
import { z, ZodError } from "zod";
import { parse } from "@std/toml";
-const ConfigSchema = z.object({
+export const ConfigSchema = z.object({
server: z.object({
port: z.number().default(Number(Deno.env.get("PORT")) || 8282),
host: z.string().default(Deno.env.get("HOST") || "127.0.0.1"),
diff --git a/src/lib/helpers/youtubePlayerHandling.ts b/src/lib/helpers/youtubePlayerHandling.ts
index 7b5f0c1..62eb30c 100644
--- a/src/lib/helpers/youtubePlayerHandling.ts
+++ b/src/lib/helpers/youtubePlayerHandling.ts
@@ -1,8 +1,8 @@
import { ApiResponse, Innertube, YT } from "youtubei.js";
import { generateRandomString } from "youtubei.js/Utils";
import { compress, decompress } from "brotli";
-import type { BG } from "bgutils";
import { Metrics } from "../helpers/metrics.ts";
+import type { TokenMinter } from "../jobs/potoken.ts";
let youtubePlayerReqLocation = "youtubePlayerReq";
if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) {
if (Deno.env.has("DENO_COMPILED")) {
@@ -31,7 +31,7 @@ export const youtubePlayerParsing = async ({
innertubeClient: Innertube;
videoId: string;
config: Config;
- tokenMinter: BG.WebPoMinter;
+ tokenMinter: TokenMinter;
metrics: Metrics | undefined;
overrideCache?: boolean;
}): Promise<object> => {
diff --git a/src/lib/helpers/youtubePlayerReq.ts b/src/lib/helpers/youtubePlayerReq.ts
index 884fae4..af78269 100644
--- a/src/lib/helpers/youtubePlayerReq.ts
+++ b/src/lib/helpers/youtubePlayerReq.ts
@@ -1,6 +1,6 @@
import { ApiResponse, Innertube } from "youtubei.js";
import NavigationEndpoint from "youtubei.js/NavigationEndpoint";
-import type { BG } from "bgutils";
+import type { TokenMinter } from "../jobs/potoken.ts";
import type { Config } from "./config.ts";
@@ -8,7 +8,7 @@ export const youtubePlayerReq = async (
innertubeClient: Innertube,
videoId: string,
config: Config,
- tokenMinter: BG.WebPoMinter,
+ tokenMinter: TokenMinter,
): Promise<ApiResponse> => {
const innertubeClientOauthEnabled = config.youtube_session.oauth_enabled;
@@ -21,7 +21,7 @@ export const youtubePlayerReq = async (
watchEndpoint: { videoId: videoId },
});
- const contentPoToken = await tokenMinter.mintAsWebsafeString(videoId);
+ const contentPoToken = await tokenMinter(videoId);
return watch_endpoint.call(innertubeClient.actions, {
playbackContext: {
diff --git a/src/lib/jobs/potoken.ts b/src/lib/jobs/potoken.ts
index e7da3c0..fe99e9c 100644
--- a/src/lib/jobs/potoken.ts
+++ b/src/lib/jobs/potoken.ts
@@ -1,6 +1,3 @@
-import { BG, buildURL, GOOG_API_KEY, USER_AGENT } from "bgutils";
-import type { WebPoSignalOutput } from "bgutils";
-import { JSDOM } from "jsdom";
import { Innertube, UniversalCache } from "youtubei.js";
import {
youtubePlayerParsing,
@@ -21,121 +18,134 @@ if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) {
}
const { getFetchClient } = await import(getFetchClientLocation);
-// Adapted from https://github.com/LuanRT/BgUtils/blob/main/examples/node/index.ts
-export const poTokenGenerate = async (
- innertubeClient: Innertube,
- config: Config,
- innertubeClientCache: UniversalCache,
- metrics: Metrics | undefined,
-): Promise<{ innertubeClient: Innertube; tokenMinter: BG.WebPoMinter }> => {
- if (innertubeClient.session.po_token) {
- innertubeClient = await Innertube.create({
- enable_session_cache: false,
- user_agent: USER_AGENT,
- retrieve_player: false,
- });
- }
+import { InputMessage, OutputMessageSchema } from "./worker.ts";
- const fetchImpl = await getFetchClient(config);
-
- const visitorData = innertubeClient.session.context.client.visitorData;
-
- if (!visitorData) {
- throw new Error("Could not get visitor data");
- }
-
- const dom = new JSDOM(
- '<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>',
- {
- url: "https://www.youtube.com/",
- referrer: "https://www.youtube.com/",
- userAgent: USER_AGENT,
- },
- );
-
- Object.assign(globalThis, {
- window: dom.window,
- document: dom.window.document,
- location: dom.window.location,
- origin: dom.window.origin,
- });
+interface TokenGeneratorWorker extends Omit<Worker, "postMessage"> {
+ postMessage(message: InputMessage): void;
+}
- if (!Reflect.has(globalThis, "navigator")) {
- Object.defineProperty(globalThis, "navigator", {
- value: dom.window.navigator,
+const workers: TokenGeneratorWorker[] = [];
+
+function createMinter(worker: TokenGeneratorWorker) {
+ return (videoId: string): Promise<string> => {
+ const { promise, resolve } = Promise.withResolvers<string>();
+ // generate a UUID to identify the request as many minter calls
+ // may be made within a timespan, and this function will be
+ // informed about all of them until it's got its own
+ const requestId = crypto.randomUUID();
+ const listener = (message: MessageEvent) => {
+ const parsedMessage = OutputMessageSchema.parse(message.data);
+ if (
+ parsedMessage.type === "content-token" &&
+ parsedMessage.requestId === requestId
+ ) {
+ worker.removeEventListener("message", listener);
+ resolve(parsedMessage.contentToken);
+ }
+ };
+ worker.addEventListener("message", listener);
+ worker.postMessage({
+ type: "content-token-request",
+ videoId,
+ requestId,
});
- }
-
- const challengeResponse = await innertubeClient.getAttestationChallenge(
- "ENGAGEMENT_TYPE_UNBOUND",
- );
- if (!challengeResponse.bg_challenge) {
- throw new Error("Could not get challenge");
- }
-
- const interpreterUrl = challengeResponse.bg_challenge.interpreter_url
- .private_do_not_access_or_else_trusted_resource_url_wrapped_value;
- const bgScriptResponse = await fetchImpl(
- `http:${interpreterUrl}`,
- );
- const interpreterJavascript = await bgScriptResponse.text();
- if (interpreterJavascript) {
- new Function(interpreterJavascript)();
- } else throw new Error("Could not load VM");
+ return promise;
+ };
+}
- // Botguard currently surfaces a "Not implemented" error here, due to the environment
- // not having a valid Canvas API in JSDOM. At the time of writing, this doesn't cause
- // any issues as the Canvas check doesn't appear to be an enforced element of the checks
- console.log(
- '[INFO] the "Not implemented: HTMLCanvasElement.prototype.getContext" error is normal. Please do not open a bug report about it.',
- );
- const botguard = await BG.BotGuardClient.create({
- program: challengeResponse.bg_challenge.program,
- globalName: challengeResponse.bg_challenge.global_name,
- globalObj: globalThis,
- });
+export type TokenMinter = ReturnType<typeof createMinter>;
- const webPoSignalOutput: WebPoSignalOutput = [];
- const botguardResponse = await botguard.snapshot({ webPoSignalOutput });
- const requestKey = "O43z0dpjhgX20SCx4KAo";
+// Adapted from https://github.com/LuanRT/BgUtils/blob/main/examples/node/index.ts
+export const poTokenGenerate = (
+ config: Config,
+ innertubeClientCache: UniversalCache,
+ metrics: Metrics | undefined,
+): Promise<{ innertubeClient: Innertube; tokenMinter: TokenMinter }> => {
+ const { promise, resolve, reject } = Promise.withResolvers<
+ Awaited<ReturnType<typeof poTokenGenerate>>
+ >();
- const integrityTokenResponse = await fetchImpl(
- buildURL("GenerateIT", true),
+ const worker: TokenGeneratorWorker = new Worker(
+ new URL("./worker.ts", import.meta.url).href,
{
- method: "POST",
- headers: {
- "content-type": "application/json+protobuf",
- "x-goog-api-key": GOOG_API_KEY,
- "x-user-agent": "grpc-web-javascript/0.1",
- "user-agent": USER_AGENT,
- },
- body: JSON.stringify([requestKey, botguardResponse]),
+ type: "module",
+ name: "PO Token Generator",
},
);
+ // take note of the worker so we can kill it once a new one takes its place
+ workers.push(worker);
+ worker.addEventListener("message", async (event) => {
+ const parsedMessage = OutputMessageSchema.parse(event.data);
+
+ // worker is listening for messages
+ if (parsedMessage.type === "ready") {
+ const untypedPostMessage = worker.postMessage.bind(worker);
+ worker.postMessage = (message: InputMessage) =>
+ untypedPostMessage(message);
+ worker.postMessage({ type: "initialise", config });
+ }
- const response = await integrityTokenResponse.json() as unknown[];
-
- if (typeof response[0] !== "string") {
- throw new Error("Could not get integrity token");
- }
+ if (parsedMessage.type === "error") {
+ console.log({ errorFromWorker: parsedMessage.error });
+ worker.terminate();
+ reject(parsedMessage.error);
+ }
- const integrityTokenBasedMinter = await BG.WebPoMinter.create({
- integrityToken: response[0],
- }, webPoSignalOutput);
+ // worker is initialised and has passed back a session token and visitor data
+ if (parsedMessage.type === "initialised") {
+ try {
+ const instantiatedInnertubeClient = await Innertube.create({
+ enable_session_cache: false,
+ po_token: parsedMessage.sessionPoToken,
+ visitor_data: parsedMessage.visitorData,
+ fetch: getFetchClient(config),
+ cache: innertubeClientCache,
+ generate_session_locally: true,
+ });
+ const minter = createMinter(worker);
+ // check token from minter
+ await checkToken({
+ instantiatedInnertubeClient,
+ config,
+ integrityTokenBasedMinter: minter,
+ metrics,
+ });
+ console.log("Successfully generated PO token");
+ const numberToKill = workers.length - 1;
+ for (let i = 0; i < numberToKill; i++) {
+ const workerToKill = workers.shift();
+ workerToKill?.terminate();
+ }
+ return resolve({
+ innertubeClient: instantiatedInnertubeClient,
+ tokenMinter: minter,
+ });
+ } catch (err) {
+ console.log("Failed to get valid PO token, will retry", {
+ err,
+ });
+ worker.terminate();
+ reject(err);
+ }
+ }
+ });
- const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(
- visitorData,
- );
+ return promise;
+};
- const instantiatedInnertubeClient = await Innertube.create({
- enable_session_cache: false,
- po_token: sessionPoToken,
- visitor_data: visitorData,
- fetch: getFetchClient(config),
- cache: innertubeClientCache,
- generate_session_locally: true,
- });
+async function checkToken({
+ instantiatedInnertubeClient,
+ config,
+ integrityTokenBasedMinter,
+ metrics,
+}: {
+ instantiatedInnertubeClient: Innertube;
+ config: Config;
+ integrityTokenBasedMinter: TokenMinter;
+ metrics: Metrics | undefined,
+}) {
+ const fetchImpl = getFetchClient(config);
if (config.jobs.youtube_session.po_token_check) {
try {
@@ -183,9 +193,4 @@ export const poTokenGenerate = async (
throw err;
}
}
-
- return {
- innertubeClient: instantiatedInnertubeClient,
- tokenMinter: integrityTokenBasedMinter,
- };
-};
+}
diff --git a/src/lib/jobs/worker.ts b/src/lib/jobs/worker.ts
new file mode 100644
index 0000000..0d62bfc
--- /dev/null
+++ b/src/lib/jobs/worker.ts
@@ -0,0 +1,231 @@
+/// <reference lib="webworker" />
+
+import { z } from "zod";
+import { Config, ConfigSchema } from "../helpers/config.ts";
+import { BG, buildURL, GOOG_API_KEY, USER_AGENT } from "bgutils";
+import type { WebPoSignalOutput } from "bgutils";
+import { JSDOM } from "jsdom";
+import { Innertube } from "youtubei.js";
+let getFetchClientLocation = "getFetchClient";
+if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) {
+ if (Deno.env.has("DENO_COMPILED")) {
+ getFetchClientLocation = Deno.mainModule.replace("src/main.ts", "") +
+ Deno.env.get("GET_FETCH_CLIENT_LOCATION");
+ } else {
+ getFetchClientLocation = Deno.env.get(
+ "GET_FETCH_CLIENT_LOCATION",
+ ) as string;
+ }
+}
+
+type FetchFunction = typeof fetch;
+const { getFetchClient }: {
+ getFetchClient: (config: Config) => Promise<FetchFunction>;
+} = await import(getFetchClientLocation);
+
+// ---- Messages to send to the webworker ----
+const InputInitialiseSchema = z.object({
+ type: z.literal("initialise"),
+ config: ConfigSchema,
+}).strict();
+
+const InputContentTokenSchema = z.object({
+ type: z.literal("content-token-request"),
+ videoId: z.string(),
+ requestId: z.string().uuid(),
+}).strict();
+export type InputInitialise = z.infer<typeof InputInitialiseSchema>;
+export type InputContentToken = z.infer<typeof InputContentTokenSchema>;
+const InputMessageSchema = z.union([
+ InputInitialiseSchema,
+ InputContentTokenSchema,
+]);
+export type InputMessage = z.infer<typeof InputMessageSchema>;
+
+// ---- Messages that the webworker sends to the parent ----
+const OutputReadySchema = z.object({
+ type: z.literal("ready"),
+}).strict();
+
+const OutputInitialiseSchema = z.object({
+ type: z.literal("initialised"),
+ sessionPoToken: z.string(),
+ visitorData: z.string(),
+}).strict();
+
+const OutputContentTokenSchema = z.object({
+ type: z.literal("content-token"),
+ contentToken: z.string(),
+ requestId: InputContentTokenSchema.shape.requestId,
+}).strict();
+
+const OutputErrorSchema = z.object({
+ type: z.literal("error"),
+ error: z.any(),
+}).strict();
+export const OutputMessageSchema = z.union([
+ OutputReadySchema,
+ OutputInitialiseSchema,
+ OutputContentTokenSchema,
+ OutputErrorSchema,
+]);
+type OutputMessage = z.infer<typeof OutputMessageSchema>;
+
+const IntegrityTokenResponse = z.tuple([z.string()]).rest(z.any());
+
+const isWorker = typeof WorkerGlobalScope !== "undefined" &&
+ self instanceof WorkerGlobalScope;
+if (isWorker) {
+ // helper function to force type-checking
+ const untypedPostmessage = self.postMessage.bind(self);
+ const postMessage = (message: OutputMessage) => {
+ untypedPostmessage(message);
+ };
+
+ let minter: BG.WebPoMinter;
+
+ onmessage = async (event) => {
+ const message = InputMessageSchema.parse(event.data);
+ if (message.type === "initialise") {
+ const fetchImpl: typeof fetch = await getFetchClient(
+ message.config,
+ );
+ try {
+ const {
+ sessionPoToken,
+ visitorData,
+ generatedMinter,
+ } = await setup({ fetchImpl });
+ minter = generatedMinter;
+ postMessage({
+ type: "initialised",
+ sessionPoToken,
+ visitorData,
+ });
+ } catch (err) {
+ postMessage({ type: "error", error: err });
+ }
+ }
+ // this is called every time a video needs a content token
+ if (message.type === "content-token-request") {
+ if (!minter) {
+ throw new Error(
+ "Minter not yet ready, must initialise first",
+ );
+ }
+ const contentToken = await minter.mintAsWebsafeString(
+ message.videoId,
+ );
+ postMessage({
+ type: "content-token",
+ contentToken,
+ requestId: message.requestId,
+ });
+ }
+ };
+
+ postMessage({ type: "ready" });
+}
+
+async function setup(
+ { fetchImpl }: { fetchImpl: FetchFunction },
+) {
+ const innertubeClient = await Innertube.create({
+ enable_session_cache: false,
+ user_agent: USER_AGENT,
+ retrieve_player: false,
+ });
+
+ const visitorData = innertubeClient.session.context.client.visitorData;
+
+ if (!visitorData) {
+ throw new Error("Could not get visitor data");
+ }
+
+ const dom = new JSDOM(
+ '<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>',
+ {
+ url: "https://www.youtube.com/",
+ referrer: "https://www.youtube.com/",
+ userAgent: USER_AGENT,
+ },
+ );
+
+ Object.assign(globalThis, {
+ window: dom.window,
+ document: dom.window.document,
+ // location: dom.window.location, // --- doesn't seem to be necessary and the Web Worker doesn't like it
+ origin: dom.window.origin,
+ });
+
+ if (!Reflect.has(globalThis, "navigator")) {
+ Object.defineProperty(globalThis, "navigator", {
+ value: dom.window.navigator,
+ });
+ }
+
+ const challengeResponse = await innertubeClient.getAttestationChallenge(
+ "ENGAGEMENT_TYPE_UNBOUND",
+ );
+ if (!challengeResponse.bg_challenge) {
+ throw new Error("Could not get challenge");
+ }
+
+ const interpreterUrl = challengeResponse.bg_challenge.interpreter_url
+ .private_do_not_access_or_else_trusted_resource_url_wrapped_value;
+ const bgScriptResponse = await fetchImpl(
+ `https:${interpreterUrl}`,
+ );
+ const interpreterJavascript = await bgScriptResponse.text();
+
+ if (interpreterJavascript) {
+ new Function(interpreterJavascript)();
+ } else throw new Error("Could not load VM");
+
+ // Botguard currently surfaces a "Not implemented" error here, due to the environment
+ // not having a valid Canvas API in JSDOM. At the time of writing, this doesn't cause
+ // any issues as the Canvas check doesn't appear to be an enforced element of the checks
+ console.log(
+ '[INFO] the "Not implemented: HTMLCanvasElement.prototype.getContext" error is normal. Please do not open a bug report about it.',
+ );
+ const botguard = await BG.BotGuardClient.create({
+ program: challengeResponse.bg_challenge.program,
+ globalName: challengeResponse.bg_challenge.global_name,
+ globalObj: globalThis,
+ });
+
+ const webPoSignalOutput: WebPoSignalOutput = [];
+ const botguardResponse = await botguard.snapshot({ webPoSignalOutput });
+ const requestKey = "O43z0dpjhgX20SCx4KAo";
+
+ const integrityTokenResponse = await fetchImpl(
+ buildURL("GenerateIT", true),
+ {
+ method: "POST",
+ headers: {
+ "content-type": "application/json+protobuf",
+ "x-goog-api-key": GOOG_API_KEY,
+ "x-user-agent": "grpc-web-javascript/0.1",
+ "user-agent": USER_AGENT,
+ },
+ body: JSON.stringify([requestKey, botguardResponse]),
+ },
+ );
+ const integrityTokenBody = IntegrityTokenResponse.parse(
+ await integrityTokenResponse.json(),
+ );
+
+ const integrityTokenBasedMinter = await BG.WebPoMinter.create({
+ integrityToken: integrityTokenBody[0],
+ }, webPoSignalOutput);
+
+ const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(
+ visitorData,
+ );
+
+ return {
+ sessionPoToken,
+ visitorData,
+ generatedMinter: integrityTokenBasedMinter,
+ };
+}
diff --git a/src/lib/types/HonoVariables.ts b/src/lib/types/HonoVariables.ts
index 1e9b7a2..ab385ab 100644
--- a/src/lib/types/HonoVariables.ts
+++ b/src/lib/types/HonoVariables.ts
@@ -1,11 +1,11 @@
import { Innertube } from "youtubei.js";
-import { BG } from "bgutils";
+import type { TokenMinter } from "../jobs/potoken.ts";
import type { Config } from "../helpers/config.ts";
import { Metrics } from "../helpers/metrics.ts";
export type HonoVariables = {
innertubeClient: Innertube;
config: Config;
- tokenMinter: BG.WebPoMinter;
+ tokenMinter: TokenMinter;
metrics: Metrics | undefined;
};
diff --git a/src/main.ts b/src/main.ts
index 851cd71..cc9e97d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,10 +1,9 @@
import { Hono } from "hono";
import { routes } from "./routes/index.ts";
import { Innertube, UniversalCache } from "youtubei.js";
-import { poTokenGenerate } from "./lib/jobs/potoken.ts";
+import { poTokenGenerate, type TokenMinter } from "./lib/jobs/potoken.ts";
import { USER_AGENT } from "bgutils";
import { retry } from "@std/async";
-import type { BG } from "bgutils";
import type { HonoVariables } from "./lib/types/HonoVariables.ts";
import { parseConfig } from "./lib/helpers/config.ts";
@@ -30,7 +29,7 @@ declare module "hono" {
const app = new Hono();
const metrics = config.server.enable_metrics ? new Metrics() : undefined;
-let tokenMinter: BG.WebPoMinter;
+let tokenMinter: TokenMinter;
let innertubeClient: Innertube;
let innertubeClientFetchPlayer = true;
const innertubeClientOauthEnabled = config.youtube_session.oauth_enabled;
@@ -65,7 +64,6 @@ if (!innertubeClientOauthEnabled) {
({ innertubeClient, tokenMinter } = await retry(
poTokenGenerate.bind(
poTokenGenerate,
- innertubeClient,
config,
innertubeClientCache,
metrics,
@@ -79,17 +77,11 @@ if (!innertubeClientOauthEnabled) {
{ backoffSchedule: [5_000, 15_000, 60_000, 180_000] },
async () => {
if (innertubeClientJobPoTokenEnabled) {
- try {
- ({ innertubeClient, tokenMinter } = await poTokenGenerate(
- innertubeClient,
- config,
- innertubeClientCache,
- metrics,
- ));
- } catch (err) {
- metrics?.potokenGenerationFailure.inc();
- throw err;
- }
+ ({ innertubeClient, tokenMinter } = await poTokenGenerate(
+ config,
+ innertubeClientCache,
+ metrics,
+ ));
} else {
innertubeClient = await Innertube.create({
enable_session_cache: false,
--
2.49.0