Compare commits

...
Sign in to create a new pull request.

32 commits

Author SHA1 Message Date
d32574db8a
ci: fix Deno stack overflow 2025-02-03 18:06:42 -03:00
62acf0f923
ci: update ci files 2025-02-03 16:58:48 -03:00
ff76e97853
chore: update default.toml file 2025-02-03 16:58:47 -03:00
77a86d4f88
style: run deno fmt 2025-02-03 16:58:47 -03:00
9de6fdc399
feat: add env variable for frequency_seconds 2025-02-03 16:58:47 -03:00
6b83617fcb
refactor: move proxy rotator logic outside of getFetchClient 2025-02-03 16:58:47 -03:00
55df6e6692
feat: add env variable for verify_requests 2025-02-03 16:58:47 -03:00
6c6345fe03
chore: update Deno to 2.1.9 in Dockerfile 2025-02-03 16:58:47 -03:00
ac0a02d184
Merge remote-tracking branch 'upstream/master'
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m23s
2025-01-28 19:33:41 -03:00
734b81590b
feat(proxy): Add support for rotating proxies
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m41s
2025-01-28 18:35:58 -03:00
4a8f0f8cd2
feat(proxy): ignore certificate errors for proxy
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m18s
2025-01-11 18:13:24 -03:00
5560d9f986
feat(metrics): better metrics and support for failed and successful requests
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m26s
2025-01-11 14:58:37 -03:00
ChunkyProgrammer
c0dafb1f7d
Add deno.yaml to lint PRs (#42)
* Add deno.yaml to lint PRs

* Rename workflow file + add deno fmt

---------

Co-authored-by: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
2024-12-30 21:35:58 +01:00
Emilien
b45db37746 fixing deno.json formatting options 2024-12-30 21:35:44 +01:00
Emilien
bd1ddb58b9 deno default formatting 2024-12-30 21:31:52 +01:00
Émilien (perso)
80f212f482
can't mkdir so creating directory in builder stage 2024-12-27 10:59:52 +00:00
Émilien (perso)
c8dc14ca93
create cache directory and set correct permissions 2024-12-27 10:52:20 +00:00
d53d10b774
add health endpoint for healthchecks (#27) 2024-12-25 11:55:41 +00:00
Emilien Devos
f89f41380a Revert "workaround issue with TV not returning any shortDescription"
This reverts commit 6ca59654ba.
2024-12-25 10:13:37 +01:00
Emilien Devos
fd76a51933 true to yes and false to no 2024-12-24 23:19:29 +01:00
Emilien Devos
f318c94bd8 include alr=false in order to disable application redirect for now
fixes #30
2024-12-24 20:57:13 +01:00
Emilien Devos
6ca59654ba workaround issue with TV not returning any shortDescription 2024-12-24 15:45:50 +01:00
Émilien (perso)
7c0e26f7f8
add section about documentation 2024-12-24 11:57:59 +00:00
12c965ceaf
add resolution limit to save bandwidth
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m8s
add environment variable for the resolution limit
2024-12-18 19:38:54 -03:00
309015454b
Merge remote-tracking branch 'upstream/master'
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m27s
2024-12-26 03:44:43 -03:00
72f3df37e6
Merge remote-tracking branch 'upstream/master'
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m3s
2024-12-24 15:20:15 -03:00
fa9a3ffb3e
support for video_cache on disk
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m2s
2024-12-21 01:20:25 -03:00
9f579c806a
add support for prometheus metrics
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 54s
2024-12-17 21:56:44 -03:00
f73ed00b6d
add support for external token generator key
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 53s
2024-12-17 03:28:49 -03:00
465878355b
add support for external token generator
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 55s
2024-12-17 01:16:59 -03:00
Emilien
8084936d5a add message about secret key size 2024-12-16 11:51:41 +01:00
Émilien (perso)
70cf366639
Update to deno 2.1.4 2024-12-16 11:46:18 +01:00
20 changed files with 443 additions and 169 deletions

View file

@ -1,68 +0,0 @@
name: Build and Push Docker Image
# Define when this workflow will run
on:
push:
branches:
- master # Trigger on pushes to master branch
tags:
- '[0-9]+.[0-9]+.[0-9]+' # Trigger on semantic version tags
paths-ignore:
- '.gitignore'
- 'LICENSE'
- 'README.md'
- 'docker-compose.yml'
workflow_dispatch: # Allow manual triggering of the workflow
# Define environment variables used throughout the workflow
env:
REGISTRY: git.nadeko.net
IMAGE_NAME: fijxu/invidious-companion
jobs:
build-and-push:
runs-on: runner
steps:
# Step 1: Check out the repository code
- name: Checkout code
uses: actions/checkout@v3
# Step 3: Set up Docker Buildx for enhanced build capabilities
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Step 4: Authenticate with Quay.io registry
- name: Login to Docker Container Registry (git.nadeko.net)
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
# Step 5: Extract metadata for Docker image tagging and labeling
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Define tagging strategy
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=sha,prefix={{branch}}-
# Define labels
labels: |
quay.expires-after=12w
# Step 6: Build and push the Docker image
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

24
.github/workflows/deno-check.yaml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Linter
on:
pull_request:
branches: [master]
jobs:
lint:
runs-on: runner
steps:
- name: Setup repo
uses: actions/checkout@v4
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Verify formatting
run: deno fmt --check src/**
- name: Run linter
run: deno lint

View file

@ -1,9 +1,15 @@
FROM i.sanxian.tech/denoland/deno:debian-2.1.3 AS builder FROM denoland/deno:debian-2.1.8 AS builder
# Fuck deno
ENV RUST_MIN_STACK=9999999999
ARG TINI_VERSION=0.19.0 ARG TINI_VERSION=0.19.0
WORKDIR /app WORKDIR /app
# cache dir for youtube.js library
RUN mkdir -p /var/tmp/youtubei.js
RUN apt update && apt install -y curl RUN apt update && apt install -y curl
COPY ./src/ /app/src/ COPY ./src/ /app/src/
@ -32,6 +38,9 @@ ENV PORT=8282 \
# Copy passwd file for the non-privileged user from the user-stage # Copy passwd file for the non-privileged user from the user-stage
COPY --from=user-stage /etc/passwd /etc/passwd COPY --from=user-stage /etc/passwd /etc/passwd
# Copy cache directory and set correct permissions
COPY --from=builder --chown=appuser /var/tmp/youtubei.js /var/tmp/youtubei.js
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app

View file

@ -6,7 +6,11 @@ Companion for Invidious which handle all the video stream retrieval from YouTube
- [deno](https://docs.deno.com/runtime/) - [deno](https://docs.deno.com/runtime/)
## Run Locally ## Documentation
- Installation guide: https://docs.invidious.io/companion-installation/
- Extra documentation for Invidious companion: https://github.com/iv-org/invidious-companion/wiki
## Run Locally (development)
``` ```
SERVER_SECRET_KEY=CHANGEME deno task dev SERVER_SECRET_KEY=CHANGEME deno task dev

View file

@ -1,9 +1,11 @@
[server] [server]
port = 8282 port = 8282
host = "127.0.0.1" host = "127.0.0.1"
secret_key = "myBeautifulKey" # secret key needs to be 16 characters long or more
secret_key = "CHANGE_ME"
base_url = "http://localhost:8282" base_url = "http://localhost:8282"
verify_requests = false verify_requests = false
# max_dash_resolution = 1080
[cache] [cache]
enabled = true enabled = true
@ -13,9 +15,9 @@ directory = "/var/tmp"
[networking] [networking]
#proxy = "" #proxy = ""
proxy_file = "proxies.txt"
# Enable YouTube new video format UMP # Enable YouTube new video format UMP
ump = false ump = false
# Some external videoplayback
# external_videoplayback_proxy = "" # external_videoplayback_proxy = ""
[jobs] [jobs]
@ -23,6 +25,7 @@ ump = false
[jobs.youtube_session] [jobs.youtube_session]
po_token_enabled = true po_token_enabled = true
frequency = "*/1 * * * *" frequency = "*/1 * * * *"
frequency_seconds = 50
[youtube_session] [youtube_session]
oauth_enabled = false oauth_enabled = false

View file

@ -1,12 +1,13 @@
{ {
"tasks": { "tasks": {
"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", "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" "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 --unsafely-ignore-certificate-errors src/main.ts"
}, },
"imports": { "imports": {
"hono": "jsr:@hono/hono@^4.6.5", "hono": "jsr:@hono/hono@^4.6.5",
"hono/logger": "jsr:@hono/hono@^4.6.5/logger", "hono/logger": "jsr:@hono/hono@^4.6.5/logger",
"hono/bearer-auth": "jsr:@hono/hono@^4.6.5/bearer-auth", "hono/bearer-auth": "jsr:@hono/hono@^4.6.5/bearer-auth",
"prom-client": "npm:prom-client@^15.1.3",
"youtubei.js": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v12.2.0-deno/deno.ts", "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/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", "youtubei.js/NavigationEndpoint": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v12.2.0-deno/deno/src/parser/classes/NavigationEndpoint.ts",
@ -20,5 +21,12 @@
"getFetchClient": "./src/lib/helpers/getFetchClient.ts", "getFetchClient": "./src/lib/helpers/getFetchClient.ts",
"googlevideo": "npm:googlevideo@2.0.0" "googlevideo": "npm:googlevideo@2.0.0"
}, },
"unstable": ["cron", "kv", "http"] "unstable": [
"cron",
"kv",
"http"
],
"fmt": {
"indentWidth": 4
}
} }

View file

@ -1,28 +1,57 @@
import { Store } from "@willsoto/node-konfig-core"; import { Store } from "@willsoto/node-konfig-core";
let proxies: string[] = [];
let currentProxyIndex = 0;
let proxyData: string;
try {
proxyData = Deno.readTextFileSync("proxies.txt");
proxies = proxyData.split("\n").map((line) => line.trim()).filter((line) =>
line.length > 0
);
} catch (error) {
console.error("[ERROR] Error reading proxy file:", error);
console.log("[INFO] Proxies from a list of proxies will not be used");
}
export const getFetchClient = (konfigStore: Store): { export const getFetchClient = (konfigStore: Store): {
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>; (input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
(input: Request | URL | string, init?: RequestInit & { (
client: Deno.HttpClient; input: Request | URL | string,
}): Promise<Response>; init?: RequestInit & {
client: Deno.HttpClient;
},
): Promise<Response>;
(input: URL | Request | string, init?: RequestInit): Promise<Response>; (input: URL | Request | string, init?: RequestInit): Promise<Response>;
} => { } => {
if (Deno.env.get("PROXY") || konfigStore.get("networking.proxy")) { if (
Deno.env.get("PROXY") || konfigStore.get("networking.proxy") ||
(proxies.length > 0)
) {
return async ( return async (
input: RequestInfo | URL, input: RequestInfo | URL,
init?: RequestInit, init?: RequestInit,
) => { ) => {
const proxyUrl = Deno.env.get("PROXY") ||
konfigStore.get("networking.proxy") as string ||
(proxies.length > 0 ? proxies[currentProxyIndex] : null);
if (!proxyUrl) {
throw new Error("No proxy available");
}
const client = Deno.createHttpClient({ const client = Deno.createHttpClient({
proxy: { proxy: {
url: Deno.env.get("PROXY") || konfigStore.get("networking.proxy") as string, url: proxyUrl,
}, },
}); });
currentProxyIndex = (currentProxyIndex + 1) % proxies.length;
const fetchRes = await fetch(input, { const fetchRes = await fetch(input, {
client, client,
headers: init?.headers, headers: init?.headers,
method: init?.method, method: init?.method,
body: init?.body, body: init?.body,
}); });
return new Response(fetchRes.body, { return new Response(fetchRes.body, {
status: fetchRes.status, status: fetchRes.status,
headers: fetchRes.headers, headers: fetchRes.headers,

View file

@ -18,7 +18,7 @@ export const konfigLoader = async (): Promise<
}; };
if (existsSync(pathJoin(Deno.cwd(), "config/local.toml"))) { if (existsSync(pathJoin(Deno.cwd(), "config/local.toml"))) {
console.log("[INFO] Using custom settings local file.") console.log("[INFO] Using custom settings local file.");
konfigFilesToLoad.files.push({ konfigFilesToLoad.files.push({
path: pathJoin(Deno.cwd(), "config/local.toml"), path: pathJoin(Deno.cwd(), "config/local.toml"),
parser: new TOMLParser(), parser: new TOMLParser(),

View file

@ -22,7 +22,11 @@ export const verifyRequest = (
); );
const encryptedData = new TextDecoder().decode( const encryptedData = new TextDecoder().decode(
decipher.decrypt(decodeBase64(stringToCheck.replace(/-/g, "+").replace(/_/g, "/"))), decipher.decrypt(
decodeBase64(
stringToCheck.replace(/-/g, "+").replace(/_/g, "/"),
),
),
); );
const [parsedTimestamp, parsedVideoId] = encryptedData.split("|"); const [parsedTimestamp, parsedVideoId] = encryptedData.split("|");
const parsedTimestampInt = parseInt(parsedTimestamp); const parsedTimestampInt = parseInt(parsedTimestamp);

View file

@ -2,6 +2,7 @@ import { ApiResponse, Innertube, YT } from "youtubei.js";
import { generateRandomString } from "youtubei.js/Utils"; import { generateRandomString } from "youtubei.js/Utils";
import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts"; import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts";
import { Store } from "@willsoto/node-konfig-core"; import { Store } from "@willsoto/node-konfig-core";
import { failedRequests, successfulRequests } from "../../routes/index.ts";
let youtubePlayerReqLocation = "youtubePlayerReq"; let youtubePlayerReqLocation = "youtubePlayerReq";
if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) { if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) {
if (Deno.env.has("DENO_COMPILED")) { if (Deno.env.has("DENO_COMPILED")) {
@ -15,7 +16,16 @@ if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) {
} }
const { youtubePlayerReq } = await import(youtubePlayerReqLocation); const { youtubePlayerReq } = await import(youtubePlayerReqLocation);
const kv = await Deno.openKv(); const videoCachePath = Deno.env.get("VIDEO_CACHE_PATH") as string ||
"/var/tmp/youtubei.js/video_cache";
export let kv: Deno.Kv;
if ((Deno.env.get("VIDEO_CACHE_ON_DISK")?.toLowerCase() ?? false) == "true") {
console.log("[INFO] Storing video cache on disk");
kv = await Deno.openKv(videoCachePath);
} else {
kv = await Deno.openKv();
}
export const youtubePlayerParsing = async ( export const youtubePlayerParsing = async (
innertubeClient: Innertube, innertubeClient: Innertube,
@ -72,6 +82,18 @@ export const youtubePlayerParsing = async (
delete videoData.streamingData.formats[index] delete videoData.streamingData.formats[index]
.signatureCipher; .signatureCipher;
} }
if (
videoData.streamingData.formats[index].url.includes(
"alr=yes",
)
) {
videoData.streamingData.formats[index].url.replace(
"alr=yes",
"alr=no",
);
} else {
videoData.streamingData.formats[index].url += "&alr=no";
}
} }
for ( for (
const [index, adaptive_format] of streamingData const [index, adaptive_format] of streamingData
@ -91,6 +113,16 @@ export const youtubePlayerParsing = async (
delete videoData.streamingData.adaptiveFormats[index] delete videoData.streamingData.adaptiveFormats[index]
.signatureCipher; .signatureCipher;
} }
if (
videoData.streamingData.adaptiveFormats[index].url
.includes("alr=yes")
) {
videoData.streamingData.adaptiveFormats[index].url
.replace("alr=yes", "alr=no");
} else {
videoData.streamingData.adaptiveFormats[index].url +=
"&alr=no";
}
} }
} }
} }
@ -114,27 +146,35 @@ export const youtubePlayerParsing = async (
invidiousCompanion: { invidiousCompanion: {
"baseUrl": Deno.env.get("SERVER_BASE_URL") || "baseUrl": Deno.env.get("SERVER_BASE_URL") ||
konfigStore.get("server.base_url") as string, konfigStore.get("server.base_url") as string,
"external_videoplayback_proxy": Deno.env.get("EXTERNAL_VIDEOPLAYBACK_PROXY") || "external_videoplayback_proxy":
konfigStore.get("networking.external_videoplayback_proxy") as string, Deno.env.get("EXTERNAL_VIDEOPLAYBACK_PROXY") ||
konfigStore.get(
"networking.external_videoplayback_proxy",
) as string,
}, },
}))(videoData); }))(videoData);
if ( if (videoData.playabilityStatus?.status == "OK") {
cacheEnabled == true && videoData.playabilityStatus?.status == "OK" successfulRequests.inc();
) { if (
(async () => { cacheEnabled == true
await kv.set( ) {
["video_cache", videoId], (async () => {
compress( await kv.set(
new TextEncoder().encode( ["video_cache", videoId],
JSON.stringify(videoOnlyNecessaryInfo), compress(
new TextEncoder().encode(
JSON.stringify(videoOnlyNecessaryInfo),
),
), ),
), {
{ expireIn: 1000 * 60 * 60,
expireIn: 1000 * 60 * 60, },
}, );
); })();
})(); }
} else {
failedRequests.inc();
} }
return videoOnlyNecessaryInfo; return videoOnlyNecessaryInfo;

View file

@ -1,30 +1,37 @@
import { Innertube, ApiResponse } from "youtubei.js"; import { ApiResponse, Innertube } from "youtubei.js";
import { Store } from "@willsoto/node-konfig-core"; import { Store } from "@willsoto/node-konfig-core";
import NavigationEndpoint from "youtubei.js/NavigationEndpoint"; import NavigationEndpoint from "youtubei.js/NavigationEndpoint";
export const youtubePlayerReq = async (innertubeClient: Innertube, videoId: string, konfigStore: Store): Promise<ApiResponse> => { export const youtubePlayerReq = async (
innertubeClient: Innertube,
videoId: string,
konfigStore: Store,
): Promise<ApiResponse> => {
const innertubeClientOauthEnabled = konfigStore.get( const innertubeClientOauthEnabled = konfigStore.get(
"youtube_session.oauth_enabled", "youtube_session.oauth_enabled",
) as boolean; ) as boolean;
let innertubeClientUsed = "WEB"; let innertubeClientUsed = "WEB";
if (innertubeClientOauthEnabled) if (innertubeClientOauthEnabled) {
innertubeClientUsed = "TV"; innertubeClientUsed = "TV";
}
const watch_endpoint = new NavigationEndpoint({ watchEndpoint: { videoId: videoId } }); const watch_endpoint = new NavigationEndpoint({
watchEndpoint: { videoId: videoId },
});
return await watch_endpoint.call(innertubeClient.actions, { return await watch_endpoint.call(innertubeClient.actions, {
playbackContext: { playbackContext: {
contentPlaybackContext: { contentPlaybackContext: {
vis: 0, vis: 0,
splay: false, splay: false,
lactMilliseconds: '-1', lactMilliseconds: "-1",
signatureTimestamp: innertubeClient.session.player?.sts signatureTimestamp: innertubeClient.session.player?.sts,
} },
}, },
serviceIntegrityDimensions: { serviceIntegrityDimensions: {
poToken: innertubeClient.session.po_token poToken: innertubeClient.session.po_token,
}, },
client: innertubeClientUsed client: innertubeClientUsed,
}); });
}; };

View file

@ -3,6 +3,7 @@ import type { BgConfig } from "bgutils";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { Innertube, UniversalCache } from "youtubei.js"; import { Innertube, UniversalCache } from "youtubei.js";
import { Store } from "@willsoto/node-konfig-core"; import { Store } from "@willsoto/node-konfig-core";
import { externalTokenGeneratorFail, poTokenFail } from "../../routes/index.ts";
let getFetchClientLocation = "getFetchClient"; let getFetchClientLocation = "getFetchClient";
if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) { if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) {
if (Deno.env.has("DENO_COMPILED")) { if (Deno.env.has("DENO_COMPILED")) {
@ -22,8 +23,62 @@ export const poTokenGenerate = async (
konfigStore: Store<Record<string, unknown>>, konfigStore: Store<Record<string, unknown>>,
innertubeClientCache: UniversalCache, innertubeClientCache: UniversalCache,
): Promise<Innertube> => { ): Promise<Innertube> => {
const externalTokenGenerator = konfigStore.get(
"server.external_token_generator",
) as string;
const externalTokenGeneratorKey = konfigStore.get(
"server.external_token_generator_key",
) as string;
const requestKey = "O43z0dpjhgX20SCx4KAo"; const requestKey = "O43z0dpjhgX20SCx4KAo";
if (externalTokenGenerator != "" && externalTokenGenerator != undefined) {
console.log(
"poTokenGenerate: using external token generator at " +
externalTokenGenerator,
);
try {
let response: Response;
if (
externalTokenGeneratorKey != "" &&
externalTokenGeneratorKey != undefined
) {
response = await fetch(
`${externalTokenGenerator}/generate`,
{
headers: {
"Authorization":
`Bearer ${externalTokenGeneratorKey}`,
},
},
);
if (response.status == 401) {
throw new Error(
`Key '${externalTokenGeneratorKey}' is invalid!`,
);
}
} else {
response = await fetch(
`${externalTokenGenerator}/generate`,
);
}
const data = await response.json();
return (await Innertube.create({
po_token: data.potoken,
visitor_data: data.visitorData,
fetch: getFetchClient(konfigStore),
cache: innertubeClientCache,
generate_session_locally: true,
}));
} catch (e) {
console.error(
"poTokenGenerate: error fetch token from the external token generator: " +
e,
);
console.log("poTokenGenerate: Using built-in token generator");
externalTokenGeneratorFail.inc();
}
}
if (innertubeClient.session.po_token) { if (innertubeClient.session.po_token) {
innertubeClient = await Innertube.create({ retrieve_player: false }); innertubeClient = await Innertube.create({ retrieve_player: false });
} }
@ -51,6 +106,7 @@ export const poTokenGenerate = async (
const bgChallenge = await BG.Challenge.create(bgConfig); const bgChallenge = await BG.Challenge.create(bgConfig);
if (!bgChallenge) { if (!bgChallenge) {
poTokenFail.inc();
throw new Error("Could not get challenge"); throw new Error("Could not get challenge");
} }
@ -59,7 +115,10 @@ export const poTokenGenerate = async (
if (interpreterJavascript) { if (interpreterJavascript) {
new Function(interpreterJavascript)(); new Function(interpreterJavascript)();
} else throw new Error("Could not load VM"); } else {
poTokenFail.inc();
throw new Error("Could not load VM");
}
const poTokenResult = await BG.PoToken.generate({ const poTokenResult = await BG.PoToken.generate({
program: bgChallenge.program, program: bgChallenge.program,

View file

@ -57,6 +57,10 @@ innertubeClient = await Innertube.create({
cookie: innertubeClientCookies || undefined, cookie: innertubeClientCookies || undefined,
}); });
const poTokenRefreshInterval = Deno.env.get("FREQUENCY_SECONDS") ||
konfigStore.get("jobs.youtube_session.frequency_seconds");
console.log("[INFO] po_token refresh interval set to", poTokenRefreshInterval);
if (!innertubeClientOauthEnabled) { if (!innertubeClientOauthEnabled) {
if (innertubeClientJobPoTokenEnabled) { if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate( innertubeClient = await poTokenGenerate(
@ -67,17 +71,12 @@ if (!innertubeClientOauthEnabled) {
} }
setInterval( setInterval(
async () => { async () => {
const currentDateTime = new Date();
console.log("regenrate token called: " + currentDateTime)
if (innertubeClientJobPoTokenEnabled) { if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate( innertubeClient = await poTokenGenerate(
innertubeClient, innertubeClient,
konfigStore, konfigStore,
innertubeClientCache, innertubeClientCache,
); );
const currentDateTimexd = new Date();
console.log("regenerate token finished: " + currentDateTimexd)
console.log("po_token: " + innertubeClient.session.po_token)
} else { } else {
innertubeClient = await Innertube.create({ innertubeClient = await Innertube.create({
cache: innertubeClientCache, cache: innertubeClientCache,
@ -85,7 +84,8 @@ if (!innertubeClientOauthEnabled) {
}); });
} }
}, },
konfigStore.get("jobs.youtube_session.frequency_seconds") as number * 1000 || 50000); poTokenRefreshInterval as number * 1000 || 50000,
);
} else if (innertubeClientOauthEnabled) { } else if (innertubeClientOauthEnabled) {
// Fired when waiting for the user to authorize the sign in attempt. // Fired when waiting for the user to authorize the sign in attempt.
innertubeClient.session.on("auth-pending", (data) => { innertubeClient.session.on("auth-pending", (data) => {
@ -117,11 +117,11 @@ app.use("*", async (c, next) => {
}); });
routes(app, konfigStore); routes(app, konfigStore);
const https = Deno.env.get("HTTPS"); const https = Deno.env.get("HTTPS");
const port = konfigStore.get("server.port") as number; const port = konfigStore.get("server.port") as number;
const host = konfigStore.get("server.host") as string; const host = konfigStore.get("server.host") as string;
if (https == "TRUE" || https == "true") { if (https == "TRUE" || https == "true") {
const cert = Deno.readTextFileSync("/data/cert.pem"); const cert = Deno.readTextFileSync("/data/cert.pem");
const key = Deno.readTextFileSync("/data/key.key"); const key = Deno.readTextFileSync("/data/key.key");

12
src/routes/health.ts Normal file
View file

@ -0,0 +1,12 @@
import { Hono } from "hono";
const health = new Hono();
health.get("/", () => {
return new Response("OK", {
status: 200,
headers: { "Content-Type": "text/plain" },
});
});
export default health;

View file

@ -2,24 +2,91 @@ import { Hono } from "hono";
import { logger } from "hono/logger"; import { logger } from "hono/logger";
import { Store } from "@willsoto/node-konfig-core"; import { Store } from "@willsoto/node-konfig-core";
import { bearerAuth } from "hono/bearer-auth"; import { bearerAuth } from "hono/bearer-auth";
import { Counter, Gauge, Registry } from "prom-client";
import youtubeApiPlayer from "./youtube_api_routes/player.ts"; import youtubeApiPlayer from "./youtube_api_routes/player.ts";
import invidiousRouteLatestVersion from "./invidious_routes/latestVersion.ts"; import invidiousRouteLatestVersion from "./invidious_routes/latestVersion.ts";
import invidiousRouteDashManifest from "./invidious_routes/dashManifest.ts"; import invidiousRouteDashManifest from "./invidious_routes/dashManifest.ts";
import videoPlaybackProxy from "./videoPlaybackProxy.ts"; import videoPlaybackProxy from "./videoPlaybackProxy.ts";
import metrics from "./metrics.ts";
export const routes = (app: Hono, konfigStore: Store<Record<string, unknown>>) => { const METRICS_PREFIX = "invidious_companion_";
app.use("*", logger()); export const register = new Registry();
app.use( export const externalTokenGeneratorFail = new Counter({
"/youtubei/v1/*", name: `${METRICS_PREFIX}externaltokengenerator_fail`,
bearerAuth({ help: "TODO",
token: Deno.env.get("SERVER_SECRET_KEY") || konfigStore.get("server.secret_key") as string, registers: [register],
}), });
);
app.route("/youtubei/v1", youtubeApiPlayer); export const cachedEntries = new Gauge({
app.route("/latest_version", invidiousRouteLatestVersion); name: `${METRICS_PREFIX}cached_entries`,
app.route("/api/manifest/dash/id", invidiousRouteDashManifest); help: "TODO",
app.route("/videoplayback", videoPlaybackProxy); registers: [register],
});
export const subreasonProtectCommunity = new Counter({
name: `${METRICS_PREFIX}subreason_protect_community`,
help: "TODO",
registers: [register],
});
export const poTokenFail = new Counter({
name: `${METRICS_PREFIX}potoken_fail`,
help: "TODO",
registers: [register],
});
export const reasonBot = new Counter({
name: `${METRICS_PREFIX}reason_bot`,
help: "TODO",
registers: [register],
});
export const videoUnavailable = new Counter({
name: `${METRICS_PREFIX}video_unavailable`,
help: "TODO",
registers: [register],
});
export const videoRestricted = new Counter({
name: `${METRICS_PREFIX}video_restricted`,
help: "TODO",
registers: [register],
});
export const failedRequests = new Counter({
name: `${METRICS_PREFIX}failed_requests`,
help: "TODO",
registers: [register],
});
export const successfulRequests = new Counter({
name: `${METRICS_PREFIX}successful_requests`,
help: "TODO",
registers: [register],
});
import health from "./health.ts";
export const routes = (
app: Hono,
konfigStore: Store<Record<string, unknown>>,
) => {
app.use("*", logger());
app.use(
"/youtubei/v1/*",
bearerAuth({
token: Deno.env.get("SERVER_SECRET_KEY") ||
konfigStore.get("server.secret_key") as string,
}),
);
app.route("/youtubei/v1", youtubeApiPlayer);
app.route("/latest_version", invidiousRouteLatestVersion);
app.route("/api/manifest/dash/id", invidiousRouteDashManifest);
app.route("/videoplayback", videoPlaybackProxy);
app.route("/metrics", metrics);
app.route("/healthz", health);
}; };

View file

@ -6,9 +6,7 @@ import {
youtubePlayerParsing, youtubePlayerParsing,
youtubeVideoInfo, youtubeVideoInfo,
} from "../../lib/helpers/youtubePlayerHandling.ts"; } from "../../lib/helpers/youtubePlayerHandling.ts";
import { import { verifyRequest } from "../../lib/helpers/verifyRequest.ts";
verifyRequest
} from "../../lib/helpers/verifyRequest.ts";
import { HTTPException } from "hono/http-exception"; import { HTTPException } from "hono/http-exception";
const dashManifest = new Hono<{ Variables: HonoVariables }>(); const dashManifest = new Hono<{ Variables: HonoVariables }>();
@ -24,11 +22,17 @@ dashManifest.get("/:videoId", async (c) => {
Record<string, unknown> Record<string, unknown>
>; >;
if (konfigStore.get("server.verify_requests") && check == undefined) { const maxDashResolution = Deno.env.get("SERVER_MAX_DASH_RESOLUTION") ||
konfigStore.get("server.max_dash_resolution") as number;
const verifyRequests = Deno.env.get("VERIFY_REQUESTS") ||
konfigStore.get("server.verify_requests");
if (verifyRequests && check == undefined) {
throw new HTTPException(400, { throw new HTTPException(400, {
res: new Response("No check ID."), res: new Response("No check ID."),
}); });
} else if (konfigStore.get("server.verify_requests") && check) { } else if (verifyRequests && check) {
if (verifyRequest(check, videoId, konfigStore) === false) { if (verifyRequest(check, videoId, konfigStore) === false) {
throw new HTTPException(400, { throw new HTTPException(400, {
res: new Response("ID incorrect."), res: new Response("ID incorrect."),
@ -67,7 +71,12 @@ dashManifest.get("/:videoId", async (c) => {
).includes("av01") ).includes("av01")
) { ) {
if (i.mime_type.includes("av01")) { if (i.mime_type.includes("av01")) {
return true; // @ts-ignore 'i.height' is possibly 'undefined'.
if (i.height > maxDashResolution) {
return false;
} else {
return true;
}
} else { } else {
return false; return false;
} }
@ -91,8 +100,11 @@ dashManifest.get("/:videoId", async (c) => {
let dashUrl = url; let dashUrl = url;
if (local) { if (local) {
// Can't create URL type without host part // Can't create URL type without host part
dashUrl = (konfigStore.get("networking.external_videoplayback_proxy") as string ?? "") + (dashUrl.pathname + dashUrl.search + "&host=" + dashUrl = (konfigStore.get(
dashUrl.host) as unknown as URL; "networking.external_videoplayback_proxy",
) as string ?? "") +
(dashUrl.pathname + dashUrl.search + "&host=" +
dashUrl.host) as unknown as URL;
if (konfigStore.get("networking.ump") as boolean) { if (konfigStore.get("networking.ump") as boolean) {
dashUrl = dashUrl + "&ump=1" as unknown as URL; dashUrl = dashUrl + "&ump=1" as unknown as URL;
} }

View file

@ -7,9 +7,7 @@ import {
youtubePlayerParsing, youtubePlayerParsing,
youtubeVideoInfo, youtubeVideoInfo,
} from "../../lib/helpers/youtubePlayerHandling.ts"; } from "../../lib/helpers/youtubePlayerHandling.ts";
import { import { verifyRequest } from "../../lib/helpers/verifyRequest.ts";
verifyRequest
} from "../../lib/helpers/verifyRequest.ts";
const latestVersion = new Hono<{ Variables: HonoVariables }>(); const latestVersion = new Hono<{ Variables: HonoVariables }>();
@ -28,12 +26,14 @@ latestVersion.get("/", async (c) => {
const konfigStore = await c.get("konfigStore") as Store< const konfigStore = await c.get("konfigStore") as Store<
Record<string, unknown> Record<string, unknown>
>; >;
const verifyRequests = Deno.env.get("VERIFY_REQUESTS") ||
konfigStore.get("server.verify_requests");
if (konfigStore.get("server.verify_requests") && check == undefined) { if (verifyRequests && check == undefined) {
throw new HTTPException(400, { throw new HTTPException(400, {
res: new Response("No check ID."), res: new Response("No check ID."),
}); });
} else if (konfigStore.get("server.verify_requests") && check) { } else if (verifyRequests && check) {
if (verifyRequest(check, id, konfigStore) === false) { if (verifyRequest(check, id, konfigStore) === false) {
throw new HTTPException(400, { throw new HTTPException(400, {
res: new Response("ID incorrect."), res: new Response("ID incorrect."),
@ -71,7 +71,9 @@ latestVersion.get("/", async (c) => {
const itagUrlParsed = new URL(itagUrl); const itagUrlParsed = new URL(itagUrl);
let urlToRedirect = itagUrlParsed.toString(); let urlToRedirect = itagUrlParsed.toString();
if (local) { if (local) {
urlToRedirect = (konfigStore.get("networking.external_videoplayback_proxy") as string ?? "") + urlToRedirect = (konfigStore.get(
"networking.external_videoplayback_proxy",
) as string ?? "") +
itagUrlParsed.pathname + itagUrlParsed.search + itagUrlParsed.pathname + itagUrlParsed.search +
"&host=" + itagUrlParsed.host; "&host=" + itagUrlParsed.host;
} }

19
src/routes/metrics.ts Normal file
View file

@ -0,0 +1,19 @@
import { Hono } from "hono";
import { cachedEntries, register } from "./index.ts";
import { kv } from "../lib/helpers/youtubePlayerHandling.ts";
const metrics = new Hono();
metrics.get("/", async () => {
let i = 0;
const entries = kv.list({ prefix: ["video_cache"] });
for await (const _ of entries) {
i += 1;
}
cachedEntries.set(i);
return new Response(await register.metrics(), {
headers: { "Content-Type": "text/plain" },
});
});
export default metrics;

View file

@ -52,10 +52,6 @@ videoPlaybackProxy.get("/", async (c) => {
// deno-lint-ignore prefer-const // deno-lint-ignore prefer-const
let queryParams = new URLSearchParams(urlReq.search); let queryParams = new URLSearchParams(urlReq.search);
queryParams.delete("host"); queryParams.delete("host");
// alr parameter is only for WEB/HTML5 clients
if (client.includes("WEB")) {
queryParams.append("alr", "yes");
}
if (rangeHeader) { if (rangeHeader) {
queryParams.append( queryParams.append(
"range", "range",

View file

@ -3,21 +3,68 @@ import { youtubePlayerParsing } from "../../lib/helpers/youtubePlayerHandling.ts
import { HonoVariables } from "../../lib/types/HonoVariables.ts"; import { HonoVariables } from "../../lib/types/HonoVariables.ts";
import { Innertube } from "youtubei.js"; import { Innertube } from "youtubei.js";
import { Store } from "@willsoto/node-konfig-core"; import { Store } from "@willsoto/node-konfig-core";
import {
reasonBot,
subreasonProtectCommunity,
videoRestricted,
videoUnavailable,
} from "../index.ts";
const player = new Hono<{ Variables: HonoVariables }>(); const player = new Hono<{ Variables: HonoVariables }>();
const errors = [
{
check: (yt: object) =>
// @ts-ignore: Property 'playabilityStatus' does not exist on type 'object'
yt.playabilityStatus?.reason?.includes(
"Sign in to confirm youre not a bot",
),
action: () => reasonBot.inc(),
},
{
check: (yt: object) =>
// @ts-ignore: Property 'playabilityStatus' does not exist on type 'object'
yt.playabilityStatus?.errorScreen?.playerErrorMessageRenderer
?.subreason?.runs?.[0]?.text?.includes(
"This helps protect our community",
),
action: () => subreasonProtectCommunity.inc(),
},
{
check: (yt: object) =>
// @ts-ignore: Property 'playabilityStatus' does not exist on type 'object'
yt.playabilityStatus?.errorScreen?.playerErrorMessageRenderer
?.subreason?.runs?.[0]?.text?.includes("Video unavailable"),
action: () => videoUnavailable.inc(),
},
{
check: (yt: object) =>
// @ts-ignore: Property 'playabilityStatus' does not exist on type 'object'
yt.playabilityStatus?.reason?.includes("This video is restricted"),
action: () => videoRestricted.inc(),
},
];
player.post("/player", async (c) => { player.post("/player", async (c) => {
const jsonReq = await c.req.json(); const jsonReq = await c.req.json();
const innertubeClient = await c.get("innertubeClient") as Innertube; const innertubeClient = await c.get("innertubeClient") as Innertube;
// @ts-ignore Do not understand how to fix this error. // @ts-ignore Do not understand how to fix this error.
const konfigStore = await c.get("konfigStore") as Store< const konfigStore = await c.get("konfigStore") as Store<
Record<string, unknown> Record<string, unknown>
>; >;
if (jsonReq.videoId) { if (jsonReq.videoId) {
return c.json( const yt = await youtubePlayerParsing(
await youtubePlayerParsing(innertubeClient, jsonReq.videoId, konfigStore) innertubeClient,
); jsonReq.videoId,
} konfigStore,
);
errors.forEach((error) => {
if (error.check(yt)) {
error.action();
}
});
return c.json(yt);
}
}); });
export default player; export default player;