Compare commits
32 commits
Author | SHA1 | Date | |
---|---|---|---|
d32574db8a | |||
62acf0f923 | |||
ff76e97853 | |||
77a86d4f88 | |||
9de6fdc399 | |||
6b83617fcb | |||
55df6e6692 | |||
6c6345fe03 | |||
ac0a02d184 | |||
734b81590b | |||
4a8f0f8cd2 | |||
5560d9f986 | |||
|
c0dafb1f7d | ||
|
b45db37746 | ||
|
bd1ddb58b9 | ||
|
80f212f482 | ||
|
c8dc14ca93 | ||
d53d10b774 | |||
|
f89f41380a | ||
|
fd76a51933 | ||
|
f318c94bd8 | ||
|
6ca59654ba | ||
|
7c0e26f7f8 | ||
12c965ceaf | |||
309015454b | |||
72f3df37e6 | |||
fa9a3ffb3e | |||
9f579c806a | |||
f73ed00b6d | |||
465878355b | |||
|
8084936d5a | ||
|
70cf366639 |
20 changed files with 443 additions and 169 deletions
|
@ -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
24
.github/workflows/deno-check.yaml
vendored
Normal 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
|
11
Dockerfile
11
Dockerfile
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
deno.json
12
deno.json
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
14
src/main.ts
14
src/main.ts
|
@ -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
12
src/routes/health.ts
Normal 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;
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
19
src/routes/metrics.ts
Normal 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;
|
|
@ -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",
|
||||||
|
|
|
@ -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 you’re 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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue