diff --git a/config/default.toml b/config/default.toml index a15b916..9e5ba0d 100644 --- a/config/default.toml +++ b/config/default.toml @@ -2,9 +2,13 @@ port = 8282 host = "127.0.0.1" hmac_key = "CHANGE_ME" +base_url = "http://localhost:8282" [cache] enabled = true [networking] -#proxy = "" \ No newline at end of file +#proxy = "" + +[jobs] +po_token_cron = "0 * * * *" \ No newline at end of file diff --git a/deno.json b/deno.json index 65cc48b..0638191 100644 --- a/deno.json +++ b/deno.json @@ -4,15 +4,15 @@ "compile": "deno compile --output invidious_companion --allow-net --allow-env --allow-sys=hostname --allow-read src/main.ts" }, "imports": { - "@std/assert": "jsr:@std/assert@1", "hono": "jsr:@hono/hono@^4.6.5", "hono/logger": "jsr:@hono/hono@^4.6.5/logger", "hono/bearer-auth": "jsr:@hono/hono@^4.6.5/bearer-auth", "youtubei.js": "npm:youtubei.js@10.5.0/web", "jsdom": "https://esm.sh/jsdom@25.0.1", "bgutils": "https://esm.sh/bgutils-js@3.0.0", - "node-config": "npm:config@3.3.12", - "toml": "npm:toml@3.0.0", + "@willsoto/node-konfig-core": "https://esm.sh/@willsoto/node-konfig-core@5.0.0", + "@willsoto/node-konfig-file": "https://esm.sh/@willsoto/node-konfig-file@3.0.0", + "@willsoto/node-konfig-toml-parser": "https://esm.sh/@willsoto/node-konfig-toml-parser@3.0.0", "youtubePlayerReq": "./src/lib/helpers/youtubePlayerReq.ts" }, "unstable": ["cron", "kv", "http"] diff --git a/deno.lock b/deno.lock index 7c55329..fd961a2 100644 --- a/deno.lock +++ b/deno.lock @@ -3,13 +3,23 @@ "packages": { "specifiers": { "jsr:@hono/hono@^4.6.5": "jsr:@hono/hono@4.6.5", - "npm:config@3.3.12": "npm:config@3.3.12", - "npm:toml@3.0.0": "npm:toml@3.0.0", + "jsr:@std/fs": "jsr:@std/fs@1.0.4", + "jsr:@std/path": "jsr:@std/path@1.0.6", + "jsr:@std/path@^1.0.6": "jsr:@std/path@1.0.6", "npm:youtubei.js@10.5.0": "npm:youtubei.js@10.5.0" }, "jsr": { "@hono/hono@4.6.5": { "integrity": "68efe4a0ab7c4fb082cb71aa894a25e1c6cfad6d124dc943471e6758a7a1bdee" + }, + "@std/fs@1.0.4": { + "integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c", + "dependencies": [ + "jsr:@std/path@^1.0.6" + ] + }, + "@std/path@1.0.6": { + "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" } }, "npm": { @@ -25,26 +35,12 @@ "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dependencies": {} }, - "config@3.3.12": { - "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", - "dependencies": { - "json5": "json5@2.2.3" - } - }, "jintr@2.1.1": { "integrity": "sha512-89cwX4ouogeDGOBsEVsVYsnWWvWjchmwXBB4kiBhmjOKw19FiOKhNhMhpxhTlK2ctl7DS+d/ethfmuBpzoNNgA==", "dependencies": { "acorn": "acorn@8.13.0" } }, - "json5@2.2.3": { - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dependencies": {} - }, - "toml@3.0.0": { - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "dependencies": {} - }, "tslib@2.8.0": { "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "dependencies": {} @@ -66,22 +62,26 @@ } } }, - "redirects": { - "https://deno.land/x/brotli/mod.ts": "https://deno.land/x/brotli@0.1.7/mod.ts" - }, "remote": { "https://deno.land/std@0.159.0/encoding/ascii85.ts": "f2b9cb8da1a55b3f120d3de2e78ac993183a4fd00dfa9cb03b51cf3a75bc0baa", "https://deno.land/x/brotli@0.1.7/mod.ts": "08b913e51488b6e7fa181f2814b9ad087fdb5520041db0368f8156bfa45fd73e", "https://deno.land/x/brotli@0.1.7/wasm.js": "77771b89e89ec7ff6e3e0939a7fb4f9b166abec3504cec0532ad5c127d6f35d2", "https://deno.land/x/lz4@v0.1.2/mod.ts": "4decfc1a3569d03fd1813bd39128b71c8f082850fe98ecfdde20025772916582", "https://deno.land/x/lz4@v0.1.2/wasm.js": "b9c65605327ba273f0c76a6dc596ec534d4cda0f0225d7a94ebc606782319e46", + "https://esm.sh/@willsoto/node-konfig-core@5.0.0": "c9674093947a209fa06eaaa84fc039b5c64e60090b24af3f9c04e651825b39df", + "https://esm.sh/@willsoto/node-konfig-file@3.0.0": "701049153614b9cce5658772cc5bf319cbc3f764b3b6d6de8b0a0e568c60fef7", + "https://esm.sh/@willsoto/node-konfig-toml-parser@3.0.0": "1eea4c4405787d23c3e8ed4ed1b955496e8367b4a05962b25aec9bc0154be371", "https://esm.sh/bgutils-js@3.0.0": "cc8243ff36620e02845bb12565036f93e464066cb0ec21b94b44562373e236bd", "https://esm.sh/jsdom@25.0.1": "74c4782b56ddcf7cd1079c1d5e31832f0adbbb4099a116e360b5e0523444832f", + "https://esm.sh/v135/@willsoto/node-konfig-core@5.0.0/denonext/node-konfig-core.mjs": "94b17731548b6f604e942023f7cb065c614002e1c4577ba269ddfaa2abcc96e0", + "https://esm.sh/v135/@willsoto/node-konfig-file@3.0.0/denonext/node-konfig-file.mjs": "273590da81e45fb62d2264f2fe9e71576d761a98224a9e378a3853196ad82b95", + "https://esm.sh/v135/@willsoto/node-konfig-toml-parser@3.0.0/denonext/node-konfig-toml-parser.mjs": "7ed9898984d6f723a5899f27d2afdcb883eff4da3d3d12ba215d966041ff338f", "https://esm.sh/v135/agent-base@7.1.0/denonext/agent-base.mjs": "0cfda332cb4694510eaeaa42dc88eed2223a9e6bed7352727a15ff163ee6b285", "https://esm.sh/v135/agent-base@7.1.1/denonext/agent-base.mjs": "e7f92e882f955036b3054644e3b01623bfe61065102ddfffb98099966dad628c", "https://esm.sh/v135/bgutils-js@3.0.0/denonext/bgutils-js.mjs": "de89db45dde68ef5ada5a3041611cc2083db2bccbdfdafbd773e937d3536de4d", "https://esm.sh/v135/bufferutil@4.0.8/denonext/bufferutil.mjs": "60a4618cbd1a5cb24935c55590b793d4ecb33862357d32e1d4614a0bbb90947f", "https://esm.sh/v135/canvas@2.11.2/denonext/canvas.mjs": "4245b1d01d91b5e807b85e40e98efe28c93634260bd8cb5ac0da71c42098a1a4", + "https://esm.sh/v135/cockatiel@2.0.2/denonext/cockatiel.mjs": "333aa0d3f0d06e4cee30fb66ce882a3385485763f10caf7ce20276f41a9a2680", "https://esm.sh/v135/cssstyle@4.1.0/denonext/cssstyle.mjs": "efe3d039b97cde71be202193e728d3f23636eca5301df3a83d21527202c4bf38", "https://esm.sh/v135/data-urls@5.0.0/denonext/data-urls.mjs": "0a38da21608a5cf482ce7f18e78b24277fc6a63e58df0f3ec210b2810026ada5", "https://esm.sh/v135/debug@4.3.4/denonext/debug.mjs": "d2ebf776ea77aa7df1b4460eb2a4aab245a9d5b19f11fa1db25f756b350bae9d", @@ -96,6 +96,7 @@ "https://esm.sh/v135/iconv-lite@0.6.3/denonext/iconv-lite.mjs": "768e37377191ab3c7414bbb15fce202a328c411da0429764984c79b8bc65abd4", "https://esm.sh/v135/is-potential-custom-element-name@1.0.1/denonext/is-potential-custom-element-name.mjs": "de2781ef99795b662f43c0840c3dcfdc303f9e60a75e66924370f902133469ed", "https://esm.sh/v135/jsdom@25.0.1/denonext/jsdom.mjs": "166355ac54ff09711e7c6e8182ea68a008045d4921a8ef67fca627bc4a2998cd", + "https://esm.sh/v135/lodash@4.17.21/denonext/lodash.mjs": "f04a5db09228738fd8cd06b6d1eaf3463b1b639d1529cf11673c3ac7bda1b1a8", "https://esm.sh/v135/ms@2.1.2/denonext/ms.mjs": "aa4dc45ba72554c5011168f8910cc646c37af53cfff1a15a4decced838b8eb14", "https://esm.sh/v135/node-gyp-build@4.6.1/denonext/node-gyp-build.mjs": "5d28b312f145a6cb2ec0dbdd80a7d34c0e0e6b5dcada65411d8bcff6c8991cc6", "https://esm.sh/v135/node-gyp-build@4.8.1/denonext/node-gyp-build.mjs": "cddfc39c5f2d6e228fb1cd8cc36a594d870470b01348f866a7fb4e6f3ed8c66d", @@ -107,6 +108,7 @@ "https://esm.sh/v135/symbol-tree@3.2.4/denonext/symbol-tree.mjs": "67199d1e47bd6e5b7d2715dd04d25658061c95fc4464f7d200b6aab9e439b5f4", "https://esm.sh/v135/tldts-core@6.1.47/denonext/tldts-core.mjs": "1ec163f0c44c05ab278859568719445a467f99d71ff63fe873a696e45560cbf1", "https://esm.sh/v135/tldts@6.1.47/denonext/tldts.mjs": "9d166ad2aa7f9753aac76569b46d48ccf1e846401c268d12b8f1b7bc92c5522b", + "https://esm.sh/v135/toml@3.0.0/denonext/toml.mjs": "067506d3e46560e450b498b3306c9f0338f05a6b2b8d1e262044ece4aa622a55", "https://esm.sh/v135/tough-cookie@5.0.0/denonext/tough-cookie.mjs": "13a12fd7e56bd78bc54df00af86674dbffecd4bf9995a46b51ef45da09431cb9", "https://esm.sh/v135/tr46@5.0.0/denonext/tr46.mjs": "66ea6f0789e30702596b0c5d0c2c2ae3e511aab829bb5b696938f61cd309e0dd", "https://esm.sh/v135/utf-8-validate@6.0.4/denonext/utf-8-validate.mjs": "ab4990b545a45f10f7711c69046ee3e9c5b732b9781937f922cefd3fc99d0e88", @@ -125,9 +127,6 @@ "workspace": { "dependencies": [ "jsr:@hono/hono@^4.6.5", - "jsr:@std/assert@1", - "npm:config@3.3.12", - "npm:toml@3.0.0", "npm:youtubei.js@10.5.0" ] } diff --git a/src/lib/helpers/konfigLoader.ts b/src/lib/helpers/konfigLoader.ts new file mode 100644 index 0000000..c41b242 --- /dev/null +++ b/src/lib/helpers/konfigLoader.ts @@ -0,0 +1,33 @@ +import { Store } from "@willsoto/node-konfig-core"; +import { FileLoader as KonfigFileLoader } from "@willsoto/node-konfig-file"; +import { TOMLParser } from "@willsoto/node-konfig-toml-parser"; +import { join as pathJoin } from "jsr:@std/path"; +import { existsSync } from "jsr:@std/fs"; + +export const konfigLoader = async (): Promise< + Store> +> => { + const konfigStore = new Store(); + const konfigFilesToLoad = { + files: [ + { + path: pathJoin(Deno.cwd(), "config/default.toml"), + parser: new TOMLParser(), + }, + ], + }; + + if (existsSync(pathJoin(Deno.cwd(), "config/local.toml"))) { + konfigFilesToLoad.files.push({ + path: pathJoin(Deno.cwd(), "config/local.toml"), + parser: new TOMLParser(), + }); + } + + const konfigLoader = new KonfigFileLoader(konfigFilesToLoad); + // @ts-ignore Safe to ignore + konfigStore.registerLoader(konfigLoader); + await konfigStore.init(); + + return konfigStore; +}; diff --git a/src/lib/helpers/youtubePlayerHandling.ts b/src/lib/helpers/youtubePlayerHandling.ts new file mode 100644 index 0000000..f03aba9 --- /dev/null +++ b/src/lib/helpers/youtubePlayerHandling.ts @@ -0,0 +1,123 @@ +import { Innertube, YT, ApiResponse } from "youtubei.js"; +import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts"; +import { youtubePlayerReq } from "youtubePlayerReq"; +import { Store } from "@willsoto/node-konfig-core"; + +const kv = await Deno.openKv(); + +export const youtubePlayerParsing = async ( + innertubeClient: Innertube, + videoId: string, + konfigStore: Store, +): Promise => { + const videoCached = (await kv.get(["video_cache", videoId])) + .value as Uint8Array; + + if (videoCached != null) { + return JSON.parse(new TextDecoder().decode(decompress(videoCached))); + } else { + const youtubePlayerResponse = await youtubePlayerReq( + innertubeClient, + videoId, + ); + const videoData = youtubePlayerResponse.data; + + const video = new YT.VideoInfo( + [youtubePlayerResponse], + innertubeClient.actions, + "", + ); + + const streamingData = video.streaming_data; + + // Modify the original YouTube response to include deciphered URLs + if (streamingData && videoData && videoData.streamingData) { + streamingData.adaptive_formats; + for (const [index, format] of streamingData.formats.entries()) { + videoData.streamingData.formats[index].url = format.decipher( + innertubeClient.session.player, + ); + if ( + videoData.streamingData.formats[index].signatureCipher !== + undefined + ) { + delete videoData.streamingData.formats[index] + .signatureCipher; + } + } + for ( + const [index, adaptive_format] of streamingData.adaptive_formats + .entries() + ) { + videoData.streamingData.adaptiveFormats[index].url = + adaptive_format + .decipher( + innertubeClient.session.player, + ); + if ( + videoData.streamingData.adaptiveFormats[index] + .signatureCipher !== + undefined + ) { + delete videoData.streamingData.adaptiveFormats[index] + .signatureCipher; + } + } + } + + const videoOnlyNecessaryInfo = (( + { + captions, + playabilityStatus, + storyboards, + streamingData, + videoDetails, + microformat, + }, + ) => ({ + captions, + playabilityStatus, + storyboards, + streamingData, + videoDetails, + microformat, + invidiousCompanion: { + "baseUrl": konfigStore.get("server.base_url") as string, + }, + }))(videoData); + + if (konfigStore.get("cache.enabled") == true) { + (async () => { + await kv.set( + ["video_cache", videoId], + compress( + new TextEncoder().encode( + JSON.stringify(videoOnlyNecessaryInfo), + ), + ), + { + expireIn: 1000 * 60 * 60, + }, + ); + })(); + } + + return videoOnlyNecessaryInfo; + } +}; + +export const youtubeVideoInfo = ( + innertubeClient: Innertube, + youtubePlayerResponseJson: object +): YT.VideoInfo => { + const playerResponse = { + success: true, + status_code: 200, + data: youtubePlayerResponseJson, + } as ApiResponse; + return new YT.VideoInfo( + [playerResponse], + innertubeClient.actions, + "", + ); +} \ No newline at end of file diff --git a/src/lib/helpers/youtubePlayerReq.ts b/src/lib/helpers/youtubePlayerReq.ts index 9d389ba..aedce90 100644 --- a/src/lib/helpers/youtubePlayerReq.ts +++ b/src/lib/helpers/youtubePlayerReq.ts @@ -1,6 +1,6 @@ -import { Innertube } from "youtubei.js"; +import { Innertube, ApiResponse } from "youtubei.js"; -export const youtubePlayerReq = async (innertubeClient: Innertube, videoId: string) => { +export const youtubePlayerReq = async (innertubeClient: Innertube, videoId: string): Promise => { return await innertubeClient.actions.execute("/player", { videoId: videoId, }); diff --git a/src/lib/jobs/potoken.ts b/src/lib/jobs/potoken.ts index 84f845f..22f5729 100644 --- a/src/lib/jobs/potoken.ts +++ b/src/lib/jobs/potoken.ts @@ -2,12 +2,13 @@ import { BG } from "bgutils"; import type { BgConfig } from "bgutils"; import { JSDOM } from "jsdom"; import { Innertube, UniversalCache } from "youtubei.js"; -import Config from "node-config"; +import { Store } from "@willsoto/node-konfig-core"; +// Adapted from https://github.com/LuanRT/BgUtils/blob/main/examples/node/index.ts export const poTokenGenerate = async ( innertubeClient: Innertube, - config: Config, -) => { + konfigStore: Store>, +): Promise => { const requestKey = "O43z0dpjhgX20SCx4KAo"; if (innertubeClient.session.po_token) { @@ -58,14 +59,14 @@ export const poTokenGenerate = async ( let fetchMethod = fetch; - if (config.has("networking.proxy")) { + if (konfigStore.get("networking.proxy")) { fetchMethod = async ( input: RequestInfo | URL, init?: RequestInit, ) => { const client = Deno.createHttpClient({ proxy: { - url: config.get("networking.proxy"), + url: konfigStore.get("networking.proxy") as string, }, }); const fetchRes = await fetch(input, { diff --git a/src/main.ts b/src/main.ts index f03d52b..a2b768b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,28 +2,31 @@ import { Hono } from "hono"; import { routes } from "./routes/index.ts"; import { Innertube } from "youtubei.js"; import { poTokenGenerate } from "./lib/jobs/potoken.ts"; -import config from "node-config"; -// deno-lint-ignore no-unused-vars -import toml from "toml"; +import { konfigLoader } from "./lib/helpers/konfigLoader.ts"; const app = new Hono(); +const konfigStore = await konfigLoader(); + let innertubeClient = await Innertube.create({ retrieve_player: false }); -innertubeClient = await poTokenGenerate(innertubeClient, config); +innertubeClient = await poTokenGenerate(innertubeClient, konfigStore); -Deno.cron("regenerate poToken", "*/10 * * * *", async () => { - innertubeClient = await poTokenGenerate(innertubeClient, config); +Deno.cron("regenerate poToken", konfigStore.get("jobs.po_token_cron"), async () => { + innertubeClient = await poTokenGenerate(innertubeClient, konfigStore); }); app.use("*", async (c, next) => { // @ts-ignore Do not understand how to fix this error. c.set("innertubeClient", innertubeClient); // @ts-ignore Do not understand how to fix this error. - c.set("config", config) + c.set("konfigStore", konfigStore); await next(); }); -routes(app); +routes(app, konfigStore); -Deno.serve({ port: config.get("server.port"), hostname: config.get("server.host") }, app.fetch); +Deno.serve({ + port: konfigStore.get("server.port") as number, + hostname: konfigStore.get("server.host") as string, +}, app.fetch); diff --git a/src/routes/index.ts b/src/routes/index.ts index ae2eabe..d191881 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,19 +1,23 @@ import { Hono } from "hono"; -import { bearerAuth } from "hono/bearer-auth"; import { logger } from "hono/logger"; -import config from 'node-config'; +import { Store } from "@willsoto/node-konfig-core"; +import { bearerAuth } from "hono/bearer-auth"; -import youtube_route_player from "./youtube_routes/player.ts"; +import youtubeApiPlayer from "./youtube_api_routes/player.ts"; +import invidiousRouteLatestVersion from "./invidious_routes/latestVersion.ts"; +import invidiousRouteDashManifest from "./invidious_routes/dashManifest.ts"; -export const routes = (app: Hono) => { +export const routes = (app: Hono, konfigStore: Store>) => { app.use("*", logger()); app.use( "/youtubei/v1/*", bearerAuth({ - token: config.get("server.hmac_key"), + token: konfigStore.get("server.hmac_key") as string, }), ); - app.route("/youtubei/v1", youtube_route_player); + app.route("/youtubei/v1", youtubeApiPlayer); + app.route("/latest_version", invidiousRouteLatestVersion); + app.route("/api/manifest/dash/id", invidiousRouteDashManifest); }; diff --git a/src/routes/invidious_routes/dashManifest.ts b/src/routes/invidious_routes/dashManifest.ts new file mode 100644 index 0000000..b563a3b --- /dev/null +++ b/src/routes/invidious_routes/dashManifest.ts @@ -0,0 +1,62 @@ +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { Innertube } from "youtubei.js"; +import { HonoVariables } from "../../lib/types/HonoVariables.ts"; +import { Store } from "@willsoto/node-konfig-core"; +import { + youtubePlayerParsing, + youtubeVideoInfo, +} from "../../lib/helpers/youtubePlayerHandling.ts"; + +const dashManifest = new Hono<{ Variables: HonoVariables }>(); + +dashManifest.get("/:videoId", async (c) => { + const { videoId } = c.req.param(); + const { local } = c.req.query(); + c.header("access-control-allow-origin", "*"); + + const innertubeClient = await c.get("innertubeClient") as Innertube; + // @ts-ignore Do not understand how to fix this error. + const konfigStore = await c.get("konfigStore") as Store< + Record + >; + + const youtubePlayerResponseJson = await youtubePlayerParsing( + innertubeClient, + videoId, + konfigStore, + ); + const videoInfo = youtubeVideoInfo( + innertubeClient, + youtubePlayerResponseJson, + ); + + if (videoInfo.playability_status?.status !== "OK") { + throw ("The video can't be played: " + videoId + " due to reason: " + + videoInfo.playability_status?.reason); + } + + c.header("content-type", "application/dash+xml"); + + if (videoInfo.streaming_data) { + videoInfo.streaming_data.adaptive_formats = videoInfo + .streaming_data.adaptive_formats + .filter((i) => i.mime_type.includes("mp4")); + + const dashFile = await videoInfo.toDash( + (url) => { + if (local) { + const dashUrl = url.pathname + url.search + "&host=" + + url.host; + // Can't create URL type without host part + return dashUrl as unknown as URL; + } else { + return url; + } + }, + ); + return c.text(dashFile); + } +}); + +export default dashManifest; diff --git a/src/routes/invidious_routes/latestVersion.ts b/src/routes/invidious_routes/latestVersion.ts new file mode 100644 index 0000000..c262368 --- /dev/null +++ b/src/routes/invidious_routes/latestVersion.ts @@ -0,0 +1,67 @@ +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import { Innertube } from "youtubei.js"; +import { HonoVariables } from "../../lib/types/HonoVariables.ts"; +import { Store } from "@willsoto/node-konfig-core"; +import { + youtubePlayerParsing, + youtubeVideoInfo, +} from "../../lib/helpers/youtubePlayerHandling.ts"; + +const latestVersion = new Hono<{ Variables: HonoVariables }>(); + +latestVersion.get("/", async (c) => { + const { itag, id, local } = c.req.query(); + c.header("access-control-allow-origin", "*"); + + if (!id || !itag) { + throw new HTTPException(400, { + res: new Response("Please specify the itag and video ID."), + }); + } + + const innertubeClient = await c.get("innertubeClient") as Innertube; + // @ts-ignore Do not understand how to fix this error. + const konfigStore = await c.get("konfigStore") as Store< + Record + >; + + const youtubePlayerResponseJson = await youtubePlayerParsing( + innertubeClient, + id, + konfigStore, + ); + const videoInfo = youtubeVideoInfo( + innertubeClient, + youtubePlayerResponseJson, + ); + + if (videoInfo.playability_status?.status !== "OK") { + throw ("The video can't be played: " + id + " due to reason: " + + videoInfo.playability_status?.reason); + } + const streamingData = videoInfo.streaming_data; + const availableFormats = streamingData?.formats.concat( + streamingData.adaptive_formats, + ); + const selectedItagFormat = availableFormats?.filter((i) => + i.itag == Number(itag) + ); + if (selectedItagFormat?.length === 0) { + throw new HTTPException(400, { + res: new Response("No itag found."), + }); + } else if (selectedItagFormat) { + const itagUrl = selectedItagFormat[0].url as string; + const urlToRedirect = new URL(itagUrl); + if (local) { + return c.redirect( + urlToRedirect.pathname + urlToRedirect.search + "&host=" + + urlToRedirect.host, + ); + } + return c.redirect(urlToRedirect.toString()); + } +}); + +export default latestVersion; diff --git a/src/routes/videoPlayback.ts b/src/routes/videoPlayback.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/youtube_api_routes/player.ts b/src/routes/youtube_api_routes/player.ts new file mode 100644 index 0000000..1c9fc23 --- /dev/null +++ b/src/routes/youtube_api_routes/player.ts @@ -0,0 +1,23 @@ +import { Hono } from "hono"; +import { youtubePlayerParsing } from "../../lib/helpers/youtubePlayerHandling.ts"; +import { HonoVariables } from "../../lib/types/HonoVariables.ts"; +import { Innertube } from "youtubei.js"; +import { Store } from "@willsoto/node-konfig-core"; + +const player = new Hono<{ Variables: HonoVariables }>(); + +player.post("/player", async (c) => { + const jsonReq = await c.req.json(); + const innertubeClient = await c.get("innertubeClient") as Innertube; + // @ts-ignore Do not understand how to fix this error. + const konfigStore = await c.get("konfigStore") as Store< + Record + >; + if (jsonReq.videoId) { + return c.json( + await youtubePlayerParsing(innertubeClient, jsonReq.videoId, konfigStore) + ); + } +}); + +export default player; diff --git a/src/routes/youtube_routes/player.ts b/src/routes/youtube_routes/player.ts deleted file mode 100644 index 1d4fd03..0000000 --- a/src/routes/youtube_routes/player.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Hono } from "hono"; -import { Innertube, YT } from "youtubei.js"; -import { compress, decompress } from "https://deno.land/x/brotli/mod.ts"; -import { HonoVariables } from "../../lib/types/HonoVariables.ts"; -import { youtubePlayerReq } from "../../lib/helpers/youtubePlayerReq.ts"; -import Config from "node-config"; - -const player = new Hono<{ Variables: HonoVariables }>(); - -const kv = await Deno.openKv(); - -player.post("/player", async (c) => { - const jsonReq = await c.req.json(); - if (jsonReq.videoId) { - const reqVideoId = jsonReq.videoId; - const innertubeClient: Innertube = await c.get("innertubeClient"); - // @ts-ignore Do not understand how to fix this error. - const config: Config = await c.get("config") as Config; - const videoCached = (await kv.get(["video_cache", reqVideoId])) - .value as Uint8Array; - - if (videoCached != null) { - return c.json( - JSON.parse(new TextDecoder().decode(decompress(videoCached))), - ); - } else { - const youtubePlayerResponse = await youtubePlayerReq( - innertubeClient, - reqVideoId, - ); - const videoData = youtubePlayerResponse.data; - - const video = new YT.VideoInfo( - [youtubePlayerResponse], - innertubeClient.actions, - "", - ); - - const streamingData = video.streaming_data; - - if (streamingData && videoData && videoData.streamingData) { - streamingData.adaptive_formats; - for (const [index, format] of streamingData.formats.entries()) { - videoData.streamingData.formats[index].url = format.decipher( - innertubeClient.session.player, - ); - if ( - videoData.streamingData.formats[index].signatureCipher !== undefined - ) { - delete videoData.streamingData.formats[index].signatureCipher; - } - } - for ( - const [index, adaptive_format] of streamingData.adaptive_formats - .entries() - ) { - videoData.streamingData.adaptiveFormats[index].url = adaptive_format - .decipher( - innertubeClient.session.player, - ); - if ( - videoData.streamingData.adaptiveFormats[index].signatureCipher !== - undefined - ) { - delete videoData.streamingData.adaptiveFormats[index] - .signatureCipher; - } - } - } - const videoOnlyNecessaryInfo = (( - { - captions, - playabilityStatus, - storyboards, - streamingData, - videoDetails, - microformat, - }, - ) => ({ - captions, - playabilityStatus, - storyboards, - streamingData, - videoDetails, - microformat, - }))(videoData); - if (config.get("cache.enabled") == true) { - (async () => { - await kv.set( - ["video_cache", reqVideoId], - compress( - new TextEncoder().encode(JSON.stringify(videoOnlyNecessaryInfo)), - ), - { - expireIn: 1000 * 60 * 60, - }, - ); - })(); - } - return c.json(videoOnlyNecessaryInfo); - } - } -}); - -export default player;