From 7ba1425bcdbf3f88cddb5a744e0dcc4c81400151 Mon Sep 17 00:00:00 2001 From: Emilien <4016501+unixfox@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:48:18 +0100 Subject: [PATCH] add videoplayback proxy route + fix potoken generation --- .github/workflows/docker-build-push.yaml | 2 +- LICENSE | 21 +++++ deno.json | 2 +- src/lib/helpers/getFetchClient.ts | 1 + src/main.ts | 4 +- src/routes/index.ts | 2 + src/routes/videoPlayback.ts | 0 src/routes/videoPlaybackProxy.ts | 97 ++++++++++++++++++++++++ 8 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 LICENSE delete mode 100644 src/routes/videoPlayback.ts create mode 100644 src/routes/videoPlaybackProxy.ts diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/docker-build-push.yaml index 520c9a9..8a79d77 100644 --- a/.github/workflows/docker-build-push.yaml +++ b/.github/workflows/docker-build-push.yaml @@ -8,7 +8,7 @@ on: tags: - '[0-9]+.[0-9]+.[0-9]+' # Trigger on semantic version tags paths-ignore: - - 'Cargo.lock' + - '.gitignore' - 'LICENSE' - 'README.md' - 'docker-compose.yml' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3659f9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 LuanRT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/deno.json b/deno.json index cd98fa9..cc7fe5f 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "dev": "deno run --allow-net --allow-env --allow-sys=hostname --allow-write=/var/tmp/youtubei.js --watch src/main.ts", + "dev": "deno run --allow-net --allow-env --allow-sys=hostname --allow-read --allow-write=/var/tmp/youtubei.js --watch src/main.ts", "compile": "deno compile --output invidious_companion --allow-net --allow-env --allow-read --allow-sys=hostname --allow-write=/var/tmp/youtubei.js src/main.ts" }, "imports": { diff --git a/src/lib/helpers/getFetchClient.ts b/src/lib/helpers/getFetchClient.ts index 2d3bdc6..9f36f8e 100644 --- a/src/lib/helpers/getFetchClient.ts +++ b/src/lib/helpers/getFetchClient.ts @@ -5,6 +5,7 @@ export const getFetchClient = (konfigStore: Store): { (input: Request | URL | string, init?: RequestInit & { client: Deno.HttpClient; }): Promise; + (input: URL | Request | string, init?: RequestInit): Promise; } => { if (Deno.env.get("PROXY") || konfigStore.get("networking.proxy")) { return async ( diff --git a/src/main.ts b/src/main.ts index 06c54f9..f8ae878 100644 --- a/src/main.ts +++ b/src/main.ts @@ -47,7 +47,7 @@ innertubeClient = await Innertube.create({ }); if (!innertubeClientOauthEnabled) { - if (innertubeClientOauthEnabled) { + if (innertubeClientJobPoTokenEnabled) { innertubeClient = await poTokenGenerate( innertubeClient, konfigStore, @@ -58,7 +58,7 @@ if (!innertubeClientOauthEnabled) { "regenerate youtube session", konfigStore.get("jobs.youtube_session.frequency") as string, async () => { - if (innertubeClientOauthEnabled) { + if (innertubeClientJobPoTokenEnabled) { innertubeClient = await poTokenGenerate( innertubeClient, konfigStore, diff --git a/src/routes/index.ts b/src/routes/index.ts index 624b594..8d6c6ec 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -6,6 +6,7 @@ import { bearerAuth } from "hono/bearer-auth"; import youtubeApiPlayer from "./youtube_api_routes/player.ts"; import invidiousRouteLatestVersion from "./invidious_routes/latestVersion.ts"; import invidiousRouteDashManifest from "./invidious_routes/dashManifest.ts"; +import videoPlaybackProxy from "./videoPlaybackProxy.ts"; export const routes = (app: Hono, konfigStore: Store>) => { app.use("*", logger()); @@ -20,4 +21,5 @@ export const routes = (app: Hono, konfigStore: Store>) = app.route("/youtubei/v1", youtubeApiPlayer); app.route("/latest_version", invidiousRouteLatestVersion); app.route("/api/manifest/dash/id", invidiousRouteDashManifest); + app.route("/videoplayback", videoPlaybackProxy); }; diff --git a/src/routes/videoPlayback.ts b/src/routes/videoPlayback.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/routes/videoPlaybackProxy.ts b/src/routes/videoPlaybackProxy.ts new file mode 100644 index 0000000..52a9be3 --- /dev/null +++ b/src/routes/videoPlaybackProxy.ts @@ -0,0 +1,97 @@ +import { Hono } from "hono"; +import { Store } from "@willsoto/node-konfig-core"; +import { getFetchClient } from "../lib/helpers/getFetchClient.ts"; +import { HTTPException } from "hono/http-exception"; + +const videoPlaybackProxy = new Hono(); + +videoPlaybackProxy.get("/", async (c) => { + const { host, c: client } = c.req.query(); + const urlReq = new URL(c.req.url); + + if (host == undefined || !/[\w-]+.googlevideo.com/.test(host)) { + throw new HTTPException(400, { + res: new Response("Host do not match or undefined."), + }); + } + + // @ts-ignore Do not understand how to fix this error. + const konfigStore = await c.get("konfigStore") as Store< + Record + >; + + // deno-lint-ignore prefer-const + let queryParams = new URLSearchParams(urlReq.search); + queryParams.delete("host"); + queryParams.append("alr", "yes"); + if (c.req.header("range")) { + queryParams.append( + "range", + (c.req.header("range") as string).split("=")[1], + ); + } + + const headersToSend: HeadersInit = { + "accept": "*/*", + "accept-encoding": "gzip, deflate, br, zstd", + "accept-language": "en-us,en;q=0.5", + "origin": "https://www.youtube.com", + "referer": "https://www.youtube.com", + }; + + if (client == "ANDROID") { + headersToSend["user-agent"] = + "com.google.android.youtube/1537338816 (Linux; U; Android 13; en_US; ; Build/TQ2A.230505.002; Cronet/113.0.5672.24)"; + } else if (client == "IOS") { + headersToSend["user-agent"] = + "com.google.ios.youtube/19.32.8 (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)"; + } else { + headersToSend["user-agent"] = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; + } + + const fetchClient = await getFetchClient(konfigStore); + + let googlevideoResponse = await fetchClient.call( + undefined, + `https://${host}/videoplayback?${queryParams.toString()}`, + { + method: "POST", + body: new Uint8Array([0x78, 0]), // protobuf: { 15: 0 } (no idea what it means but this is what YouTube uses), + headers: headersToSend, + }, + ); + + if (googlevideoResponse.headers.has("location")) { + googlevideoResponse = await fetchClient.call( + undefined, + googlevideoResponse.headers.get("location") as string, + { + method: "POST", + body: new Uint8Array([0x78, 0]), // protobuf: { 15: 0 } (no idea what it means but this is what YouTube uses) + headers: headersToSend, + }, + ); + } + + return new Response(googlevideoResponse.body, { + status: googlevideoResponse.status, + statusText: googlevideoResponse.statusText, + headers: { + "content-length": + googlevideoResponse.headers.get("content-length") || "", + "access-control-allow-origin": "*", + "accept-ranges": googlevideoResponse.headers.get("accept-ranges") || + "", + "cache-control": googlevideoResponse.headers.get("cache-control") || + "", + "content-type": googlevideoResponse.headers.get("content-type") || + "", + "expires": googlevideoResponse.headers.get("expires") || "", + "last-modified": googlevideoResponse.headers.get("last-modified") || + "", + }, + }); +}); + +export default videoPlaybackProxy;