Compare commits

...

11 commits

Author SHA1 Message Date
Emilien
306f2d5f12 cut key for 16 size
Some checks are pending
Build and Push Docker Image / build-and-push (push) Waiting to run
2024-12-13 21:24:39 +01:00
Emilien
c446f337de add ability to verify the requests
fixes #9
2024-12-13 21:12:49 +01:00
Emilien
100e2722a9 better replace for Deno.mainModule 2024-12-12 23:34:05 +01:00
Emilien
f3fff5cc09 trying to improve again dynamic import 2024-12-12 23:28:49 +01:00
Emilien
57f4da848b improving dynamic import 2024-12-12 22:48:03 +01:00
Emilien
10cbeab3dc update to youtube.js v12.0.0-deno 2024-12-12 21:45:57 +01:00
Emilien
45003ebabd fixing await dynamic import 2024-12-12 21:36:38 +01:00
Emilien
a4519724e8 fix new usage with youtube.js v12 + fix av01 default usage 2024-12-11 21:40:39 +01:00
Emilien
5aba65b89e handle new changes from youtube.js v12 2024-12-09 22:16:42 +01:00
Emilien
1c1b4cc697 switch to deno 2 2024-12-09 21:58:34 +01:00
Emilien
596713c44c update to youtube.js v12.0.0 2024-12-09 20:57:04 +01:00
13 changed files with 2373 additions and 193 deletions

View file

@ -1,4 +1,4 @@
FROM denoland/deno:debian-1.46.3 AS builder
FROM denoland/deno:debian-2.1.3 AS builder
ARG TINI_VERSION=0.19.0

1
compile.env Normal file
View file

@ -0,0 +1 @@
DENO_COMPILED=true

View file

@ -3,6 +3,7 @@ port = 8282
host = "127.0.0.1"
secret_key = "CHANGE_ME"
base_url = "http://localhost:8282"
verify_requests = false
[cache]
enabled = true

View file

@ -1,22 +1,23 @@
{
"tasks": {
"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"
"dev": "deno run --allow-import=github.com:443,jsr.io:443,raw.githubusercontent.com:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname --allow-read --allow-write=/var/tmp/youtubei.js --watch src/main.ts",
"compile": "deno compile --include ./src/lib/helpers/youtubePlayerReq.ts --include ./src/lib/helpers/getFetchClient.ts --output invidious_companion --allow-import=github.com:443,jsr.io:443,raw.githubusercontent.com:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-read --allow-sys=hostname --allow-write=/var/tmp/youtubei.js src/main.ts"
},
"imports": {
"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": "https://deno.land/x/youtubei@v11.0.1-deno/deno.ts",
"youtubei.js/endpoints": "https://deno.land/x/youtubei@v11.0.1-deno/deno/src/core/endpoints/index.ts",
"youtubei.js/Utils": "https://deno.land/x/youtubei@v11.0.1-deno/deno/src/utils/Utils.ts",
"jsdom": "https://esm.sh/jsdom@25.0.1",
"bgutils": "https://esm.sh/bgutils-js@3.0.0",
"youtubei.js": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v12.2.0-deno/deno.ts",
"youtubei.js/Utils": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v12.2.0-deno/deno/src/utils/Utils.ts",
"youtubei.js/NavigationEndpoint": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v12.2.0-deno/deno/src/parser/classes/NavigationEndpoint.ts",
"jsdom": "npm:jsdom@25.0.1",
"bgutils": "https://esm.sh/bgutils-js@3.1.0",
"estree": "https://esm.sh/@types/estree@1.0.6",
"@willsoto/node-konfig-core": "npm:@willsoto/node-konfig-core@5.0.0",
"@willsoto/node-konfig-file": "npm:@willsoto/node-konfig-file@3.0.0",
"@willsoto/node-konfig-toml-parser": "npm:@willsoto/node-konfig-toml-parser@3.0.0",
"youtubePlayerReq": "./src/lib/helpers/youtubePlayerReq.ts",
"getFetchClient": "./src/lib/helpers/getFetchClient.ts",
"googlevideo": "npm:googlevideo@2.0.0"
},
"unstable": ["cron", "kv", "http"]

2244
deno.lock

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
import { Store } from "@willsoto/node-konfig-core";
import { decodeBase64 } from "jsr:@std/encoding/base64";
import { Aes } from "https://deno.land/x/crypto@v0.10.1/aes.ts";
import {
Ecb,
Padding,
} from "https://deno.land/x/crypto@v0.10.1/block-modes.ts";
export const verifyRequest = (
stringToCheck: string,
videoId: string,
konfigStore: Store,
): boolean => {
try {
const decipher = new Ecb(
Aes,
new TextEncoder().encode((
Deno.env.get("SERVER_SECRET_KEY") ||
konfigStore.get("server.secret_key") as string
).substring(0, 16)),
Padding.PKCS7,
);
const encryptedData = new TextDecoder().decode(
decipher.decrypt(decodeBase64(stringToCheck)),
);
const [parsedTimestamp, parsedVideoId] = encryptedData.split("|");
const parsedTimestampInt = parseInt(parsedTimestamp);
const timestampNow = Math.round(+new Date() / 1000);
if (parsedVideoId !== videoId) {
return false;
}
// only allow ID to live for 6 hours
if ((timestampNow + 6 * 60 * 60) - parsedTimestampInt < 0) {
return false;
}
} catch (_) {
return false;
}
return true;
};

View file

@ -1,10 +1,19 @@
import { ApiResponse, Innertube, YT } from "youtubei.js";
import { generateRandomString } from "youtubei.js/Utils";
import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts";
const { youtubePlayerReq } = await import(
Deno.env.get("YT_PLAYER_REQ_LOCATION") || "./youtubePlayerReq.ts"
);
import { Store } from "@willsoto/node-konfig-core";
let youtubePlayerReqLocation = "youtubePlayerReq";
if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) {
if (Deno.env.has("DENO_COMPILED")) {
youtubePlayerReqLocation = Deno.mainModule.replace("src/main.ts", "") +
Deno.env.get("YT_PLAYER_REQ_LOCATION");
} else {
youtubePlayerReqLocation = Deno.env.get(
"YT_PLAYER_REQ_LOCATION",
) as string;
}
}
const { youtubePlayerReq } = await import(youtubePlayerReqLocation);
const kv = await Deno.openKv();

View file

@ -1,6 +1,6 @@
import { Innertube, ApiResponse } from "youtubei.js";
import { PlayerEndpoint } from "youtubei.js/endpoints";
import { Store } from "@willsoto/node-konfig-core";
import NavigationEndpoint from "youtubei.js/NavigationEndpoint";
export const youtubePlayerReq = async (innertubeClient: Innertube, videoId: string, konfigStore: Store): Promise<ApiResponse> => {
const innertubeClientOauthEnabled = konfigStore.get(
@ -11,13 +11,20 @@ export const youtubePlayerReq = async (innertubeClient: Innertube, videoId: stri
if (innertubeClientOauthEnabled)
innertubeClientUsed = "TV";
return await innertubeClient.actions.execute(
PlayerEndpoint.PATH, PlayerEndpoint.build({
video_id: videoId,
// @ts-ignore Unable to import type InnerTubeClient
client: innertubeClientUsed,
sts: innertubeClient.session.player?.sts,
po_token: innertubeClient.session.po_token
})
);
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: { videoId: videoId } });
return await watch_endpoint.call(innertubeClient.actions, {
playbackContext: {
contentPlaybackContext: {
vis: 0,
splay: false,
lactMilliseconds: '-1',
signatureTimestamp: innertubeClient.session.player?.sts
}
},
serviceIntegrityDimensions: {
poToken: innertubeClient.session.po_token
},
client: innertubeClientUsed
});
};

View file

@ -3,13 +3,24 @@ import type { BgConfig } from "bgutils";
import { JSDOM } from "jsdom";
import { Innertube, UniversalCache } from "youtubei.js";
import { Store } from "@willsoto/node-konfig-core";
const { getFetchClient } = await import(Deno.env.get("GET_FETCH_CLIENT_LOCATION") || "../helpers/getFetchClient.ts");
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;
}
}
const { getFetchClient } = await import(getFetchClientLocation);
// Adapted from https://github.com/LuanRT/BgUtils/blob/main/examples/node/index.ts
export const poTokenGenerate = async (
innertubeClient: Innertube,
konfigStore: Store<Record<string, unknown>>,
innertubeClientCache: UniversalCache
innertubeClientCache: UniversalCache,
): Promise<Innertube> => {
const requestKey = "O43z0dpjhgX20SCx4KAo";
@ -57,7 +68,7 @@ export const poTokenGenerate = async (
bgConfig,
});
await BG.PoToken.generatePlaceholder(visitorData);;
await BG.PoToken.generatePlaceholder(visitorData);
return (await Innertube.create({
po_token: poTokenResult.poToken,

View file

@ -3,7 +3,18 @@ import { routes } from "./routes/index.ts";
import { Innertube, UniversalCache } from "youtubei.js";
import { poTokenGenerate } from "./lib/jobs/potoken.ts";
import { konfigLoader } from "./lib/helpers/konfigLoader.ts";
const { getFetchClient } = await import(Deno.env.get("GET_FETCH_CLIENT_LOCATION") || "./lib/helpers/getFetchClient.ts");
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;
}
}
const { getFetchClient } = await import(getFetchClientLocation);
const app = new Hono();
const konfigStore = await konfigLoader();
@ -11,100 +22,101 @@ const konfigStore = await konfigLoader();
let innertubeClient: Innertube;
let innertubeClientFetchPlayer = true;
const innertubeClientOauthEnabled = konfigStore.get(
"youtube_session.oauth_enabled",
"youtube_session.oauth_enabled",
) as boolean;
const innertubeClientJobPoTokenEnabled = konfigStore.get(
"jobs.youtube_session.po_token_enabled",
"jobs.youtube_session.po_token_enabled",
) as boolean;
const innertubeClientCookies = konfigStore.get(
"jobs.youtube_session.cookies",
"jobs.youtube_session.cookies",
) as string;
let innertubeClientCache = new UniversalCache(
true,
konfigStore.get('cache.directory') as string + "/youtubei.js/",
true,
konfigStore.get("cache.directory") as string + "/youtubei.js/",
) as UniversalCache;
Deno.env.set('TMPDIR', konfigStore.get("cache.directory") as string)
Deno.env.set("TMPDIR", konfigStore.get("cache.directory") as string);
if (!innertubeClientOauthEnabled) {
if (innertubeClientJobPoTokenEnabled) {
console.log("[INFO] job po_token is active.");
// Don't fetch fetch player yet for po_token
innertubeClientFetchPlayer = false;
} else if (!innertubeClientJobPoTokenEnabled) {
console.log("[INFO] job po_token is NOT active.");
}
if (innertubeClientJobPoTokenEnabled) {
console.log("[INFO] job po_token is active.");
// Don't fetch fetch player yet for po_token
innertubeClientFetchPlayer = false;
} else if (!innertubeClientJobPoTokenEnabled) {
console.log("[INFO] job po_token is NOT active.");
}
} else if (innertubeClientOauthEnabled) {
// Can't use cache if using OAuth#cacheCredentials
innertubeClientCache = new UniversalCache(false);
// Can't use cache if using OAuth#cacheCredentials
innertubeClientCache = new UniversalCache(false);
}
innertubeClient = await Innertube.create({
cache: innertubeClientCache,
retrieve_player: innertubeClientFetchPlayer,
fetch: getFetchClient(konfigStore),
cookie: innertubeClientCookies || undefined,
cache: innertubeClientCache,
retrieve_player: innertubeClientFetchPlayer,
fetch: getFetchClient(konfigStore),
cookie: innertubeClientCookies || undefined,
});
if (!innertubeClientOauthEnabled) {
if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate(
innertubeClient,
konfigStore,
innertubeClientCache as UniversalCache,
);
}
Deno.cron(
"regenerate youtube session",
konfigStore.get("jobs.youtube_session.frequency") as string,
async () => {
if (innertubeClientJobPoTokenEnabled) {
if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate(
innertubeClient,
konfigStore,
innertubeClientCache,
innertubeClient,
konfigStore,
innertubeClientCache as UniversalCache,
);
} else {
innertubeClient = await Innertube.create({
cache: innertubeClientCache,
retrieve_player: innertubeClientFetchPlayer,
});
}
},
);
} else if (innertubeClientOauthEnabled) {
// Fired when waiting for the user to authorize the sign in attempt.
innertubeClient.session.on("auth-pending", (data) => {
console.log(
`[INFO] [OAUTH] Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`,
}
Deno.cron(
"regenerate youtube session",
konfigStore.get("jobs.youtube_session.frequency") as string,
async () => {
if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate(
innertubeClient,
konfigStore,
innertubeClientCache,
);
} else {
innertubeClient = await Innertube.create({
cache: innertubeClientCache,
retrieve_player: innertubeClientFetchPlayer,
});
}
},
);
});
// Fired when authentication is successful.
innertubeClient.session.on("auth", () => {
console.log("[INFO] [OAUTH] Sign in successful!");
});
// Fired when the access token expires.
innertubeClient.session.on("update-credentials", async () => {
console.log("[INFO] [OAUTH] Credentials updated.");
await innertubeClient.session.oauth.cacheCredentials();
});
} else if (innertubeClientOauthEnabled) {
// Fired when waiting for the user to authorize the sign in attempt.
innertubeClient.session.on("auth-pending", (data) => {
console.log(
`[INFO] [OAUTH] Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`,
);
});
// Fired when authentication is successful.
innertubeClient.session.on("auth", () => {
console.log("[INFO] [OAUTH] Sign in successful!");
});
// Fired when the access token expires.
innertubeClient.session.on("update-credentials", async () => {
console.log("[INFO] [OAUTH] Credentials updated.");
await innertubeClient.session.oauth.cacheCredentials();
});
// Attempt to sign in and then cache the credentials
await innertubeClient.session.signIn();
await innertubeClient.session.oauth.cacheCredentials();
// Attempt to sign in and then cache the credentials
await innertubeClient.session.signIn();
await innertubeClient.session.oauth.cacheCredentials();
}
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("konfigStore", konfigStore);
await 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("konfigStore", konfigStore);
await next();
});
routes(app, konfigStore);
Deno.serve({
port: Number(Deno.env.get("PORT")) || konfigStore.get("server.port") as number,
hostname: Deno.env.get("HOST") || konfigStore.get("server.host") as string,
port: Number(Deno.env.get("PORT")) ||
konfigStore.get("server.port") as number,
hostname: Deno.env.get("HOST") || konfigStore.get("server.host") as string,
}, app.fetch);

View file

@ -6,12 +6,16 @@ import {
youtubePlayerParsing,
youtubeVideoInfo,
} from "../../lib/helpers/youtubePlayerHandling.ts";
import {
verifyRequest
} from "../../lib/helpers/verifyRequest.ts";
import { HTTPException } from "hono/http-exception";
const dashManifest = new Hono<{ Variables: HonoVariables }>();
dashManifest.get("/:videoId", async (c) => {
const { videoId } = c.req.param();
const { local } = c.req.query();
const { check, local } = c.req.query();
c.header("access-control-allow-origin", "*");
const innertubeClient = await c.get("innertubeClient") as Innertube;
@ -20,6 +24,18 @@ dashManifest.get("/:videoId", async (c) => {
Record<string, unknown>
>;
if (konfigStore.get("server.verify_requests") && check == undefined) {
throw new HTTPException(400, {
res: new Response("No check ID."),
});
} else if (konfigStore.get("server.verify_requests") && check) {
if (verifyRequest(check, videoId, konfigStore) === false) {
throw new HTTPException(400, {
res: new Response("ID incorrect."),
});
}
}
const youtubePlayerResponseJson = await youtubePlayerParsing(
innertubeClient,
videoId,
@ -44,7 +60,12 @@ dashManifest.get("/:videoId", async (c) => {
.streaming_data.adaptive_formats
.filter((i) => {
if (i.mime_type.includes("mp4")) {
if (i.has_video) {
if (
i.has_video &&
JSON.stringify(
videoInfo.streaming_data?.adaptive_formats,
).includes("av01")
) {
if (i.mime_type.includes("av01")) {
return true;
} else {

View file

@ -7,11 +7,14 @@ import {
youtubePlayerParsing,
youtubeVideoInfo,
} from "../../lib/helpers/youtubePlayerHandling.ts";
import {
verifyRequest
} from "../../lib/helpers/verifyRequest.ts";
const latestVersion = new Hono<{ Variables: HonoVariables }>();
latestVersion.get("/", async (c) => {
const { itag, id, local } = c.req.query();
const { check, itag, id, local } = c.req.query();
c.header("access-control-allow-origin", "*");
if (!id || !itag) {
@ -26,6 +29,18 @@ latestVersion.get("/", async (c) => {
Record<string, unknown>
>;
if (konfigStore.get("server.verify_requests") && check == undefined) {
throw new HTTPException(400, {
res: new Response("No check ID."),
});
} else if (konfigStore.get("server.verify_requests") && check) {
if (verifyRequest(check, id, konfigStore) === false) {
throw new HTTPException(400, {
res: new Response("ID incorrect."),
});
}
}
const youtubePlayerResponseJson = await youtubePlayerParsing(
innertubeClient,
id,

View file

@ -1,7 +1,18 @@
import { Hono } from "hono";
import { Store } from "@willsoto/node-konfig-core";
const { getFetchClient } = await import(Deno.env.get("GET_FETCH_CLIENT_LOCATION") || "../lib/helpers/getFetchClient.ts");
import { HTTPException } from "hono/http-exception";
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;
}
}
const { getFetchClient } = await import(getFetchClientLocation);
const videoPlaybackProxy = new Hono();