Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Fijxu 2025-03-17 00:01:39 -03:00
commit 3ddbd0c19e
Signed by: Fijxu
GPG key ID: 32C1DDF333EDA6A4
17 changed files with 555 additions and 226 deletions

View file

@ -19,6 +19,9 @@ jobs:
- name: Verify formatting - name: Verify formatting
run: deno fmt --check src/** run: deno fmt --check src/**
- name: Verify typing
run: deno check src/**
- name: Run linter - name: Run linter
run: deno lint run: deno lint

View file

@ -19,6 +19,11 @@ RUN curl -fsSL https://github.com/krallin/tini/releases/download/v${TINI_VERSION
--output /tini \ --output /tini \
&& chmod +x /tini && chmod +x /tini
RUN arch=$(uname -m) && \
curl -fsSL https://github.com/dmikusa/tiny-health-checker/releases/download/v0.36.0/thc-${arch}-unknown-linux-musl \
--output /thc \
&& chmod +x /thc
RUN deno task compile RUN deno task compile
# Stage for creating the non-privileged user # Stage for creating the non-privileged user
@ -29,12 +34,16 @@ RUN adduser -u 10001 -S appuser
FROM gcr.io/distroless/cc FROM gcr.io/distroless/cc
COPY --from=builder /app/invidious_companion /app/ COPY --from=builder /app/invidious_companion /app/
COPY --from=builder /thc /thc
COPY ./config/ /app/config/ COPY ./config/ /app/config/
COPY --from=builder /tini /tini COPY --from=builder /tini /tini
ENV PORT=8282 \ ENV PORT=8282 \
HOST=0.0.0.0 HOST=0.0.0.0
ENV THC_PORT=${PORT} \
THC_PATH=/healthz
# 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
@ -49,4 +58,4 @@ USER appuser
ENTRYPOINT ["/tini", "--", "/app/invidious_companion"] ENTRYPOINT ["/tini", "--", "/app/invidious_companion"]
# HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD ["/tini", "--", "/app/invidious_companion", "healthcheck"] HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=5 CMD ["/thc"]

View file

@ -3,7 +3,6 @@ port = 8282
host = "127.0.0.1" host = "127.0.0.1"
# secret key needs to be 16 characters long or more # secret key needs to be 16 characters long or more
secret_key = "CHANGE_ME" secret_key = "CHANGE_ME"
base_url = "http://localhost:8282"
verify_requests = false verify_requests = false
# max_dash_resolution = 1080 # max_dash_resolution = 1080
# encrypt_query_params = false # encrypt_query_params = false

View file

@ -4,14 +4,15 @@
"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" "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.7.2",
"hono/logger": "jsr:@hono/hono@^4.6.5/logger", "hono/logger": "jsr:@hono/hono@^4.7.2/logger",
"hono/bearer-auth": "jsr:@hono/hono@^4.6.5/bearer-auth", "hono/bearer-auth": "jsr:@hono/hono@^4.7.2/bearer-auth",
"prom-client": "npm:prom-client@^15.1.3", "prom-client": "npm:prom-client@^15.1.3",
"youtubei.js": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno.ts", "youtubei.js": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno.ts",
"youtubei.js/Utils": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Utils.ts", "youtubei.js/Utils": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Utils.ts",
"youtubei.js/NavigationEndpoint": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/parser/classes/NavigationEndpoint.ts", "youtubei.js/NavigationEndpoint": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/parser/classes/NavigationEndpoint.ts",
"jsdom": "npm:jsdom@25.0.1", "youtubei.js/PlayerCaptionsTracklist": "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/parser/classes/PlayerCaptionsTracklist.ts",
"jsdom": "npm:jsdom@26.0.0",
"bgutils": "https://esm.sh/bgutils-js@3.1.0", "bgutils": "https://esm.sh/bgutils-js@3.1.0",
"estree": "https://esm.sh/@types/estree@1.0.6", "estree": "https://esm.sh/@types/estree@1.0.6",
"@willsoto/node-konfig-core": "npm:@willsoto/node-konfig-core@5.0.0", "@willsoto/node-konfig-core": "npm:@willsoto/node-konfig-core@5.0.0",
@ -19,15 +20,17 @@
"@willsoto/node-konfig-toml-parser": "npm:@willsoto/node-konfig-toml-parser@3.0.0", "@willsoto/node-konfig-toml-parser": "npm:@willsoto/node-konfig-toml-parser@3.0.0",
"youtubePlayerReq": "./src/lib/helpers/youtubePlayerReq.ts", "youtubePlayerReq": "./src/lib/helpers/youtubePlayerReq.ts",
"getFetchClient": "./src/lib/helpers/getFetchClient.ts", "getFetchClient": "./src/lib/helpers/getFetchClient.ts",
"googlevideo": "npm:googlevideo@2.0.0", "metrics": "./src/routes/metrics.ts",
"metrics": "./src/routes/metrics.ts" "googlevideo": "jsr:@luanrt/googlevideo@^2.0.0",
"jsr:@luanrt/jintr": "jsr:@luanrt/jintr@^3.2.1"
}, },
"unstable": [ "unstable": [
"cron", "cron",
"kv", "kv",
"http" "http",
"temporal"
], ],
"fmt": { "fmt": {
"indentWidth": 4 "indentWidth": 4
} }
} }

173
deno.lock generated
View file

@ -1,41 +1,38 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"jsr:@hono/hono@^4.6.5": "4.6.19", "jsr:@hono/hono@^4.7.2": "4.7.2",
"jsr:@luanrt/jintr@*": "3.2.1", "jsr:@luanrt/jintr@^3.2.1": "3.2.1",
"jsr:@std/async@*": "1.0.10", "jsr:@std/async@*": "1.0.10",
"jsr:@std/encoding@*": "1.0.6", "jsr:@std/encoding@*": "1.0.7",
"jsr:@std/fs@*": "1.0.10", "jsr:@std/fs@*": "1.0.13",
"jsr:@std/path@*": "1.0.8", "jsr:@std/path@*": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8",
"npm:@types/estree@^1.0.6": "1.0.6",
"npm:@willsoto/node-konfig-core@5.0.0": "5.0.0", "npm:@willsoto/node-konfig-core@5.0.0": "5.0.0",
"npm:@willsoto/node-konfig-file@3.0.0": "3.0.0_@willsoto+node-konfig-core@5.0.0", "npm:@willsoto/node-konfig-file@3.0.0": "3.0.0_@willsoto+node-konfig-core@5.0.0",
"npm:@willsoto/node-konfig-toml-parser@3.0.0": "3.0.0_@willsoto+node-konfig-core@5.0.0", "npm:@willsoto/node-konfig-toml-parser@3.0.0": "3.0.0_@willsoto+node-konfig-core@5.0.0",
"npm:acorn@^8.8.0": "8.14.0", "npm:acorn@^8.8.0": "8.14.0",
"npm:googlevideo@2.0.0": "2.0.0", "npm:jsdom@26.0.0": "26.0.0",
"npm:jsdom@25.0.1": "25.0.1",
"npm:prom-client@^15.1.3": "15.1.3" "npm:prom-client@^15.1.3": "15.1.3"
}, },
"jsr": { "jsr": {
"@hono/hono@4.6.19": { "@hono/hono@4.7.2": {
"integrity": "5ba1bd0ef74449c0a647f029e29896776c30fb64aefbcef8724af4ce4846791b" "integrity": "73466a24bd6eb3b527cde18e59ca5543890ddacb4312fc0bc7504d28a7e57a38"
}, },
"@luanrt/jintr@3.2.1": { "@luanrt/jintr@3.2.1": {
"integrity": "78acef6eb1c0e54303c14f77233a610dcc8e9da386f5e59c6167a2c5d8fdbe87", "integrity": "78acef6eb1c0e54303c14f77233a610dcc8e9da386f5e59c6167a2c5d8fdbe87",
"dependencies": [ "dependencies": [
"npm:@types/estree",
"npm:acorn" "npm:acorn"
] ]
}, },
"@std/async@1.0.10": { "@std/async@1.0.10": {
"integrity": "2ff1b1c7d33d1416159989b0f69e59ec7ee8cb58510df01e454def2108b3dbec" "integrity": "2ff1b1c7d33d1416159989b0f69e59ec7ee8cb58510df01e454def2108b3dbec"
}, },
"@std/encoding@1.0.6": { "@std/encoding@1.0.7": {
"integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d"
}, },
"@std/fs@1.0.10": { "@std/fs@1.0.13": {
"integrity": "bf041f9d7a0a460817f0421be8946d0e06011b3433e6c83a215628de5e3c7c2c", "integrity": "756d3ff0ade91c9e72b228e8012b6ff00c3d4a4ac9c642c4dac083536bf6c605",
"dependencies": [ "dependencies": [
"jsr:@std/path@^1.0.8" "jsr:@std/path@^1.0.8"
] ]
@ -55,21 +52,18 @@
"lru-cache" "lru-cache"
] ]
}, },
"@bufbuild/protobuf@2.2.3": { "@csstools/color-helpers@5.0.2": {
"integrity": "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==" "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="
}, },
"@csstools/color-helpers@5.0.1": { "@csstools/css-calc@2.1.2_@csstools+css-parser-algorithms@3.0.4__@csstools+css-tokenizer@3.0.3_@csstools+css-tokenizer@3.0.3": {
"integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==" "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==",
},
"@csstools/css-calc@2.1.1_@csstools+css-parser-algorithms@3.0.4__@csstools+css-tokenizer@3.0.3_@csstools+css-tokenizer@3.0.3": {
"integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==",
"dependencies": [ "dependencies": [
"@csstools/css-parser-algorithms", "@csstools/css-parser-algorithms",
"@csstools/css-tokenizer" "@csstools/css-tokenizer"
] ]
}, },
"@csstools/css-color-parser@3.0.7_@csstools+css-parser-algorithms@3.0.4__@csstools+css-tokenizer@3.0.3_@csstools+css-tokenizer@3.0.3": { "@csstools/css-color-parser@3.0.8_@csstools+css-parser-algorithms@3.0.4__@csstools+css-tokenizer@3.0.3_@csstools+css-tokenizer@3.0.3": {
"integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==",
"dependencies": [ "dependencies": [
"@csstools/color-helpers", "@csstools/color-helpers",
"@csstools/css-calc", "@csstools/css-calc",
@ -89,9 +83,6 @@
"@opentelemetry/api@1.9.0": { "@opentelemetry/api@1.9.0": {
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="
}, },
"@types/estree@1.0.6": {
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"@willsoto/node-konfig-core@5.0.0": { "@willsoto/node-konfig-core@5.0.0": {
"integrity": "sha512-1AevWxJw/9oGz53YpabBSYhNTY1fsSIxiWd6ehSLCPnlgZ1ciJy6ZcwMpAb5L86dMjFHYwTa4Z8HiOP6iHyi+Q==", "integrity": "sha512-1AevWxJw/9oGz53YpabBSYhNTY1fsSIxiWd6ehSLCPnlgZ1ciJy6ZcwMpAb5L86dMjFHYwTa4Z8HiOP6iHyi+Q==",
"dependencies": [ "dependencies": [
@ -124,6 +115,13 @@
"bintrees@1.0.2": { "bintrees@1.0.2": {
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
}, },
"call-bind-apply-helpers@1.0.2": {
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": [
"es-errors",
"function-bind"
]
},
"cockatiel@2.0.2": { "cockatiel@2.0.2": {
"integrity": "sha512-ehw7t3twohGiMTxARX0AcFiUxndXLhnIBWbnRnHtfde2jRywlPpPB/o3s9YSptXPj6tkOG0fzET4CUUx4GIpEg==" "integrity": "sha512-ehw7t3twohGiMTxARX0AcFiUxndXLhnIBWbnRnHtfde2jRywlPpPB/o3s9YSptXPj6tkOG0fzET4CUUx4GIpEg=="
}, },
@ -137,7 +135,7 @@
"integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==",
"dependencies": [ "dependencies": [
"@asamuzakjp/css-color", "@asamuzakjp/css-color",
"rrweb-cssom@0.8.0" "rrweb-cssom"
] ]
}, },
"data-urls@5.0.0": { "data-urls@5.0.0": {
@ -159,21 +157,88 @@
"delayed-stream@1.0.0": { "delayed-stream@1.0.0": {
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
}, },
"dunder-proto@1.0.1": {
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": [
"call-bind-apply-helpers",
"es-errors",
"gopd"
]
},
"entities@4.5.0": { "entities@4.5.0": {
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
}, },
"form-data@4.0.1": { "es-define-property@1.0.1": {
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors@1.3.0": {
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms@1.1.1": {
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": [
"es-errors"
]
},
"es-set-tostringtag@2.1.0": {
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dependencies": [
"es-errors",
"get-intrinsic",
"has-tostringtag",
"hasown"
]
},
"form-data@4.0.2": {
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dependencies": [ "dependencies": [
"asynckit", "asynckit",
"combined-stream", "combined-stream",
"es-set-tostringtag",
"mime-types" "mime-types"
] ]
}, },
"googlevideo@2.0.0": { "function-bind@1.1.2": {
"integrity": "sha512-OVlNWZ07TPIelaEII6mH9od+Cxljl7P4AzhEYVNN5d4FhFT9L5otpcLtgvraTE9u69KfVVw+L4pVeczArcD33w==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"get-intrinsic@1.3.0": {
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": [ "dependencies": [
"@bufbuild/protobuf" "call-bind-apply-helpers",
"es-define-property",
"es-errors",
"es-object-atoms",
"function-bind",
"get-proto",
"gopd",
"has-symbols",
"hasown",
"math-intrinsics"
]
},
"get-proto@1.0.1": {
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": [
"dunder-proto",
"es-object-atoms"
]
},
"gopd@1.2.0": {
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"has-symbols@1.1.0": {
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"has-tostringtag@1.0.2": {
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": [
"has-symbols"
]
},
"hasown@2.0.2": {
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": [
"function-bind"
] ]
}, },
"html-encoding-sniffer@4.0.0": { "html-encoding-sniffer@4.0.0": {
@ -205,8 +270,8 @@
"is-potential-custom-element-name@1.0.1": { "is-potential-custom-element-name@1.0.1": {
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
}, },
"jsdom@25.0.1": { "jsdom@26.0.0": {
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==",
"dependencies": [ "dependencies": [
"cssstyle", "cssstyle",
"data-urls", "data-urls",
@ -218,7 +283,7 @@
"is-potential-custom-element-name", "is-potential-custom-element-name",
"nwsapi", "nwsapi",
"parse5", "parse5",
"rrweb-cssom@0.7.1", "rrweb-cssom",
"saxes", "saxes",
"symbol-tree", "symbol-tree",
"tough-cookie", "tough-cookie",
@ -237,6 +302,9 @@
"lru-cache@10.4.3": { "lru-cache@10.4.3": {
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
}, },
"math-intrinsics@1.1.0": {
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"mime-db@1.52.0": { "mime-db@1.52.0": {
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
}, },
@ -268,9 +336,6 @@
"punycode@2.3.1": { "punycode@2.3.1": {
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
}, },
"rrweb-cssom@0.7.1": {
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="
},
"rrweb-cssom@0.8.0": { "rrweb-cssom@0.8.0": {
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==" "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="
}, },
@ -292,11 +357,11 @@
"bintrees" "bintrees"
] ]
}, },
"tldts-core@6.1.75": { "tldts-core@6.1.82": {
"integrity": "sha512-AOvV5YYIAFFBfransBzSTyztkc3IMfz5Eq3YluaRiEu55nn43Fzaufx70UqEKYr8BoLCach4q8g/bg6e5+/aFw==" "integrity": "sha512-Jabl32m21tt/d/PbDO88R43F8aY98Piiz6BVH9ShUlOAiiAELhEqwrAmBocjAqnCfoUeIsRU+h3IEzZd318F3w=="
}, },
"tldts@6.1.75": { "tldts@6.1.82": {
"integrity": "sha512-+lFzEXhpl7JXgWYaXcB6DqTYXbUArvrWAE/5ioq/X3CdWLbDjpPP4XTrQBmEJ91y3xbe4Fkw7Lxv4P3GWeJaNg==", "integrity": "sha512-KCTjNL9F7j8MzxgfTgjT+v21oYH38OidFty7dH00maWANAI2IsLw2AnThtTJi9HKALHZKQQWnNebYheadacD+g==",
"dependencies": [ "dependencies": [
"tldts-core" "tldts-core"
] ]
@ -304,8 +369,8 @@
"toml@3.0.0": { "toml@3.0.0": {
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
}, },
"tough-cookie@5.1.0": { "tough-cookie@5.1.2": {
"integrity": "sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dependencies": [ "dependencies": [
"tldts" "tldts"
] ]
@ -334,15 +399,15 @@
"whatwg-mimetype@4.0.0": { "whatwg-mimetype@4.0.0": {
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==" "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="
}, },
"whatwg-url@14.1.0": { "whatwg-url@14.1.1": {
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==",
"dependencies": [ "dependencies": [
"tr46", "tr46",
"webidl-conversions" "webidl-conversions"
] ]
}, },
"ws@8.18.0": { "ws@8.18.1": {
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="
}, },
"xml-name-validator@5.0.0": { "xml-name-validator@5.0.0": {
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==" "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="
@ -351,9 +416,6 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
} }
}, },
"redirects": {
"https://esm.sh/@types/estree@1.0.6": "https://esm.sh/@types/estree@1.0.6/index.d.ts"
},
"remote": { "remote": {
"https://deno.land/std@0.159.0/encoding/ascii85.ts": "f2b9cb8da1a55b3f120d3de2e78ac993183a4fd00dfa9cb03b51cf3a75bc0baa", "https://deno.land/std@0.159.0/encoding/ascii85.ts": "f2b9cb8da1a55b3f120d3de2e78ac993183a4fd00dfa9cb03b51cf3a75bc0baa",
"https://deno.land/x/brotli@0.1.7/mod.ts": "08b913e51488b6e7fa181f2814b9ad087fdb5520041db0368f8156bfa45fd73e", "https://deno.land/x/brotli@0.1.7/mod.ts": "08b913e51488b6e7fa181f2814b9ad087fdb5520041db0368f8156bfa45fd73e",
@ -1013,11 +1075,9 @@
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/platform/lib.ts": "fdac76db7d9f1c13039036f590270fe7860396a8afb24eac078122d91b4d6742", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/platform/lib.ts": "fdac76db7d9f1c13039036f590270fe7860396a8afb24eac078122d91b4d6742",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/platform/polyfills/web-crypto.ts": "ae20ed00dea9eafca9ba590f4fa440299cbd57288add788c59cb19f3455ae6d1", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/platform/polyfills/web-crypto.ts": "ae20ed00dea9eafca9ba590f4fa440299cbd57288add788c59cb19f3455ae6d1",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/Cache.ts": "06cd238bce7c9657055151587e36ee445e8236d54d27272124ced10ea7be0da4", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/Cache.ts": "06cd238bce7c9657055151587e36ee445e8236d54d27272124ced10ea7be0da4",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/DashOptions.ts": "cf694c112ab97d778b3df735ddc76fd16fd5ae0d49943e2cc580f1f986f63da6",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/FormatUtils.ts": "8962fc5f7d02fd19fb2bce937692f9aae4fb15379a40d4ad4edd2634197e7abc", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/FormatUtils.ts": "8962fc5f7d02fd19fb2bce937692f9aae4fb15379a40d4ad4edd2634197e7abc",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/Misc.ts": "205df5b124cb1e17cffccbc7cd920257af4744aea67308d18c05e7e4d12e9827", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/Misc.ts": "205df5b124cb1e17cffccbc7cd920257af4744aea67308d18c05e7e4d12e9827",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/PlatformShim.ts": "06f656f0d2bc20980ef77148455b662af10fe4b0e48d41566bf28e471eea4be1", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/PlatformShim.ts": "06f656f0d2bc20980ef77148455b662af10fe4b0e48d41566bf28e471eea4be1",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/StreamingInfoOptions.ts": "cd404a388a8d36bc28b60053851cd92495b666938552898c430c661ac4f7ad0b",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/index.ts": "a28448751e5567b91cc60528a82999885630ed099c19318f265c4360537cff4e", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/types/index.ts": "a28448751e5567b91cc60528a82999885630ed099c19318f265c4360537cff4e",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Cache.ts": "fd90c88da32e9283adf065475a5cb4e680b5152a6bc06dd8a3dc9349358cab35", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Cache.ts": "fd90c88da32e9283adf065475a5cb4e680b5152a6bc06dd8a3dc9349358cab35",
"https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Constants.ts": "148e86c63bf222845ccf24508be7d93b599543cf14e73a78a870ffe92c834857", "https://raw.githubusercontent.com/LuanRT/YouTube.js/refs/tags/v13.1.0-deno/deno/src/utils/Constants.ts": "148e86c63bf222845ccf24508be7d93b599543cf14e73a78a870ffe92c834857",
@ -1036,12 +1096,13 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@hono/hono@^4.6.5", "jsr:@hono/hono@^4.7.2",
"jsr:@luanrt/googlevideo@2",
"jsr:@luanrt/jintr@^3.2.1",
"npm:@willsoto/node-konfig-core@5.0.0", "npm:@willsoto/node-konfig-core@5.0.0",
"npm:@willsoto/node-konfig-file@3.0.0", "npm:@willsoto/node-konfig-file@3.0.0",
"npm:@willsoto/node-konfig-toml-parser@3.0.0", "npm:@willsoto/node-konfig-toml-parser@3.0.0",
"npm:googlevideo@2.0.0", "npm:jsdom@26.0.0",
"npm:jsdom@25.0.1",
"npm:prom-client@^15.1.3" "npm:prom-client@^15.1.3"
] ]
} }

View file

@ -2,8 +2,9 @@ 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, retryCount } from "metrics"; import { failedRequests, retryCount, successfulRequests } from "metrics";
import { innertubeEmbeddedClient } from "../../main.ts"; import { innertubeEmbeddedClient } from "../../main.ts";
import type { BG } from "bgutils";
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")) {
@ -30,23 +31,34 @@ if ((Deno.env.get("VIDEO_CACHE_ON_DISK")?.toLowerCase() ?? false) == "true") {
kv = await Deno.openKv(); kv = await Deno.openKv();
} }
export const youtubePlayerParsing = async ( export const youtubePlayerParsing = async ({
innertubeClient: Innertube, innertubeClient,
videoId: string, videoId,
konfigStore: Store, konfigStore,
): Promise<object> => { tokenMinter,
const cacheEnabled = konfigStore.get("cache.enabled"); overrideCache = false,
}: {
innertubeClient: Innertube;
videoId: string;
konfigStore: Store;
tokenMinter: BG.WebPoMinter;
overrideCache?: boolean;
}): Promise<object> => {
const cacheEnabled = overrideCache
? false
: konfigStore.get("cache.enabled");
const videoCached = (await kv.get(["video_cache", videoId])) const videoCached = (await kv.get(["video_cache", videoId]))
.value as Uint8Array; .value as Uint8Array;
if (videoCached != null && cacheEnabled == true) { if (videoCached != null && cacheEnabled) {
return JSON.parse(new TextDecoder().decode(decompress(videoCached))); return JSON.parse(new TextDecoder().decode(decompress(videoCached)));
} else { } else {
let youtubePlayerResponse = await youtubePlayerReq( let youtubePlayerResponse = await youtubePlayerReq(
innertubeClient, innertubeClient,
videoId, videoId,
konfigStore, konfigStore,
tokenMinter,
); );
for (let retries = 1; retries <= (maxRetries as number); retries++) { for (let retries = 1; retries <= (maxRetries as number); retries++) {
@ -57,8 +69,10 @@ export const youtubePlayerParsing = async (
) { ) {
break; break;
} }
console.log(`[DEBUG] Got 'This helps protect our community', retrying request for ${videoId}. Retry ${retries} of ${maxRetries}`) console.log(
retryCount.inc() `[DEBUG] Got 'This helps protect our community', retrying request for ${videoId}. Retry ${retries} of ${maxRetries}`,
);
retryCount.inc();
youtubePlayerResponse = await youtubePlayerReq( youtubePlayerResponse = await youtubePlayerReq(
innertubeClient, innertubeClient,
videoId, videoId,
@ -67,10 +81,14 @@ export const youtubePlayerParsing = async (
} }
if ( if (
youtubePlayerResponse.data.playabilityStatus.status === "UNPLAYABLE" || youtubePlayerResponse.data.playabilityStatus.status ===
youtubePlayerResponse.data.playabilityStatus.status === "LOGIN_REQUIRED" || "UNPLAYABLE" ||
youtubePlayerResponse.data.playabilityStatus.status === "CONTENT_CHECK_REQUIRED" || youtubePlayerResponse.data.playabilityStatus.status ===
youtubePlayerResponse.data.playabilityStatus.reason === "Sign in to confirm your age" "LOGIN_REQUIRED" ||
youtubePlayerResponse.data.playabilityStatus.status ===
"CONTENT_CHECK_REQUIRED" ||
youtubePlayerResponse.data.playabilityStatus.reason ===
"Sign in to confirm your age"
) { ) {
innertubeEmbeddedClient.session.context.client.visitorData = innertubeEmbeddedClient.session.context.client.visitorData =
innertubeClient.session.context.client.visitorData; innertubeClient.session.context.client.visitorData;
@ -186,15 +204,6 @@ export const youtubePlayerParsing = async (
streamingData, streamingData,
videoDetails, videoDetails,
microformat, microformat,
invidiousCompanion: {
"baseUrl": Deno.env.get("SERVER_BASE_URL") ||
konfigStore.get("server.base_url") as string,
"external_videoplayback_proxy":
Deno.env.get("EXTERNAL_VIDEOPLAYBACK_PROXY") ||
konfigStore.get(
"networking.external_videoplayback_proxy",
) as string,
},
}))(videoData); }))(videoData);
if (videoData.playabilityStatus?.status == "OK") { if (videoData.playabilityStatus?.status == "OK") {
@ -215,9 +224,9 @@ export const youtubePlayerParsing = async (
}, },
); );
})(); })();
} else {
failedRequests.inc();
} }
} else {
failedRequests.inc();
} }
return videoOnlyNecessaryInfo; return videoOnlyNecessaryInfo;

View file

@ -1,11 +1,13 @@
import { ApiResponse, ClientType, Innertube } from "youtubei.js"; import { ApiResponse, ClientType, 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";
import type { BG } from "bgutils";
export const youtubePlayerReq = async ( export const youtubePlayerReq = async (
innertubeClient: Innertube, innertubeClient: Innertube,
videoId: string, videoId: string,
konfigStore: Store, konfigStore: Store,
tokenMinter: BG.WebPoMinter,
): Promise<ApiResponse> => { ): Promise<ApiResponse> => {
const innertubeClientOauthEnabled = konfigStore.get( const innertubeClientOauthEnabled = konfigStore.get(
"youtube_session.oauth_enabled", "youtube_session.oauth_enabled",
@ -23,7 +25,9 @@ export const youtubePlayerReq = async (
watchEndpoint: { videoId: videoId }, watchEndpoint: { videoId: videoId },
}); });
return await watch_endpoint.call(innertubeClient.actions, { const contentPoToken = await tokenMinter.mintAsWebsafeString(videoId);
return watch_endpoint.call(innertubeClient.actions, {
playbackContext: { playbackContext: {
contentPlaybackContext: { contentPlaybackContext: {
vis: 0, vis: 0,
@ -33,7 +37,7 @@ export const youtubePlayerReq = async (
}, },
}, },
serviceIntegrityDimensions: { serviceIntegrityDimensions: {
poToken: innertubeClient.session.po_token, poToken: contentPoToken,
}, },
client: innertubeClientUsed, client: innertubeClientUsed,
}); });

View file

@ -0,0 +1,88 @@
import { Innertube } from "youtubei.js";
import type { CaptionTrackData } from "youtubei.js/PlayerCaptionsTracklist";
import { HTTPException } from "hono/http-exception";
function createTemporalDuration(milliseconds: number) {
return new Temporal.Duration(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
milliseconds,
);
}
const ESCAPE_SUBSTITUTIONS = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\u200E": "&lrm;",
"\u200F": "&rlm;",
"\u00A0": "&nbsp;",
};
export async function handleTranscripts(
innertubeClient: Innertube,
videoId: string,
selectedCaption: CaptionTrackData,
) {
const lines: string[] = ["WEBVTT"];
const info = await innertubeClient.getInfo(videoId);
const transcriptInfo = await (await info.getTranscript()).selectLanguage(
selectedCaption.name.text || "",
);
const rawTranscriptLines = transcriptInfo.transcript.content?.body
?.initial_segments;
if (rawTranscriptLines == undefined) throw new HTTPException(404);
rawTranscriptLines.forEach((line) => {
const timestampFormatOptions = {
style: "digital",
minutesDisplay: "always",
fractionalDigits: 3,
};
// Temporal.Duration.prototype.toLocaleString() is supposed to delegate to Intl.DurationFormat
// which Deno does not support. However, instead of following specs and having toLocaleString return
// the same toString() it seems to have its own implementation of Intl.DurationFormat,
// with its options parameter type incorrectly restricted to the same as the one for Intl.DateTimeFormatOptions
// even though they do not share the same arguments.
//
// The above matches the options parameter of Intl.DurationFormat, and the resulting output is as expected.
// Until this is fixed typechecking must be disabled for the two use cases below
//
// See
// https://docs.deno.com/api/web/~/Intl.DateTimeFormatOptions
// https://docs.deno.com/api/web/~/Temporal.Duration.prototype.toLocaleString
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration/toLocaleString
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat
const start_ms = createTemporalDuration(Number(line.start_ms)).round({
largestUnit: "year",
//@ts-ignore see above
}).toLocaleString("en-US", timestampFormatOptions);
const end_ms = createTemporalDuration(Number(line.end_ms)).round({
largestUnit: "year",
//@ts-ignore see above
}).toLocaleString("en-US", timestampFormatOptions);
const timestamp = `${start_ms} --> ${end_ms}`;
const text = (line.snippet?.text || "").replace(
/[&<>\u200E\u200F\u00A0]/g,
(match: string) =>
ESCAPE_SUBSTITUTIONS[
match as keyof typeof ESCAPE_SUBSTITUTIONS
],
);
lines.push(`${timestamp}\n${text}`);
});
return lines.join("\n\n");
}

View file

@ -1,9 +1,13 @@
import { BG } from "bgutils"; import { BG, buildURL, GOOG_API_KEY, USER_AGENT } from "bgutils";
import type { BgConfig } from "bgutils"; import type { WebPoSignalOutput } 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 "metrics"; import { poTokenFail } from "metrics";
import {
youtubePlayerParsing,
youtubeVideoInfo,
} from "../helpers/youtubePlayerHandling.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,96 +26,58 @@ export const poTokenGenerate = async (
innertubeClient: Innertube, innertubeClient: Innertube,
konfigStore: Store<Record<string, unknown>>, konfigStore: Store<Record<string, unknown>>,
innertubeClientCache: UniversalCache, innertubeClientCache: UniversalCache,
): Promise<Innertube> => { ): Promise<{ innertubeClient: Innertube; tokenMinter: BG.WebPoMinter }> => {
const externalTokenGenerator = konfigStore.get(
"server.external_token_generator",
) as string;
const externalTokenGeneratorKey = konfigStore.get(
"server.external_token_generator_key",
) as string;
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({
enable_session_cache: false,
user_agent: USER_AGENT,
retrieve_player: false,
});
} }
const fetchImpl = await getFetchClient(konfigStore);
const visitorData = innertubeClient.session.context.client.visitorData; const visitorData = innertubeClient.session.context.client.visitorData;
if (!visitorData) { if (!visitorData) {
throw new Error("Could not get visitor data"); throw new Error("Could not get visitor data");
} }
const dom = new JSDOM(); const dom = new JSDOM(
'<!DOCTYPE html><html lang="en"><head><title></title></head><body></body></html>',
{
url: "https://www.youtube.com/",
referrer: "https://www.youtube.com/",
userAgent: USER_AGENT,
},
);
Object.assign(globalThis, { Object.assign(globalThis, {
window: dom.window, window: dom.window,
document: dom.window.document, document: dom.window.document,
location: dom.window.location,
origin: dom.window.origin,
}); });
const bgConfig: BgConfig = { if (!Reflect.has(globalThis, "navigator")) {
fetch: getFetchClient(konfigStore), Object.defineProperty(globalThis, "navigator", {
globalObj: globalThis, value: dom.window.navigator,
identifier: visitorData, });
requestKey, }
};
const bgChallenge = await BG.Challenge.create(bgConfig); const challengeResponse = await innertubeClient.getAttestationChallenge(
"ENGAGEMENT_TYPE_UNBOUND",
if (!bgChallenge) { );
poTokenFail.inc(); if (!challengeResponse.bg_challenge) {
throw new Error("Could not get challenge"); throw new Error("Could not get challenge");
} }
const interpreterJavascript = bgChallenge.interpreterJavascript const interpreterUrl = challengeResponse.bg_challenge.interpreter_url
.privateDoNotAccessOrElseSafeScriptWrappedValue; .private_do_not_access_or_else_trusted_resource_url_wrapped_value;
const bgScriptResponse = await fetchImpl(
`http:${interpreterUrl}`,
);
const interpreterJavascript = await bgScriptResponse.text();
if (interpreterJavascript) { if (interpreterJavascript) {
new Function(interpreterJavascript)(); new Function(interpreterJavascript)();
@ -120,19 +86,103 @@ export const poTokenGenerate = async (
throw new Error("Could not load VM"); throw new Error("Could not load VM");
} }
const poTokenResult = await BG.PoToken.generate({ // Botguard currently surfaces a "Not implemented" error here, due to the environment
program: bgChallenge.program, // not having a valid Canvas API in JSDOM. At the time of writing, this doesn't cause
globalName: bgChallenge.globalName, // any issues as the Canvas check doesn't appear to be an enforced element of the checks
bgConfig, console.log(
'[INFO] the "Not implemented: HTMLCanvasElement.prototype.getContext" error is normal. Please do not open a bug report about it.',
);
const botguard = await BG.BotGuardClient.create({
program: challengeResponse.bg_challenge.program,
globalName: challengeResponse.bg_challenge.global_name,
globalObj: globalThis,
}); });
await BG.PoToken.generatePlaceholder(visitorData); const webPoSignalOutput: WebPoSignalOutput = [];
const botguardResponse = await botguard.snapshot({ webPoSignalOutput });
const requestKey = "O43z0dpjhgX20SCx4KAo";
return (await Innertube.create({ const integrityTokenResponse = await fetchImpl(
po_token: poTokenResult.poToken, buildURL("GenerateIT", true),
{
method: "POST",
headers: {
"content-type": "application/json+protobuf",
"x-goog-api-key": GOOG_API_KEY,
"x-user-agent": "grpc-web-javascript/0.1",
"user-agent": USER_AGENT,
},
body: JSON.stringify([requestKey, botguardResponse]),
},
);
const response = await integrityTokenResponse.json() as unknown[];
if (typeof response[0] !== "string") {
throw new Error("Could not get integrity token");
}
const integrityTokenBasedMinter = await BG.WebPoMinter.create({
integrityToken: response[0],
}, webPoSignalOutput);
const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString(
visitorData,
);
const instantiatedInnertubeClient = await Innertube.create({
enable_session_cache: false,
po_token: sessionPoToken,
visitor_data: visitorData, visitor_data: visitorData,
fetch: getFetchClient(konfigStore), fetch: getFetchClient(konfigStore),
cache: innertubeClientCache, cache: innertubeClientCache,
generate_session_locally: true, generate_session_locally: true,
})); });
try {
const feed = await instantiatedInnertubeClient.getTrending();
// get all videos and shuffle them randomly to avoid using the same trending video over and over
const videos = feed.videos
.filter((video) => video.type === "Video")
.map((value) => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value);
const video = videos.find((video) => "id" in video);
if (!video) {
throw new Error("no videos with id found in trending");
}
const youtubePlayerResponseJson = await youtubePlayerParsing({
innertubeClient: instantiatedInnertubeClient,
videoId: video.id,
konfigStore,
tokenMinter: integrityTokenBasedMinter,
overrideCache: true,
});
const videoInfo = youtubeVideoInfo(
instantiatedInnertubeClient,
youtubePlayerResponseJson,
);
const validFormat = videoInfo.streaming_data?.adaptive_formats[0];
if (!validFormat) {
throw new Error(
"failed to find valid video with adaptive format to check token against",
);
}
const result = await fetchImpl(validFormat?.url, { method: "HEAD" });
if (result.status !== 200) {
throw new Error(
`did not get a 200 when checking video, got ${result.status} instead`,
);
}
} catch (err) {
console.log("Failed to get valid PO token, will retry", { err });
throw err;
}
return {
innertubeClient: instantiatedInnertubeClient,
tokenMinter: integrityTokenBasedMinter,
};
}; };

View file

@ -1,7 +1,9 @@
import { Innertube } from "youtubei.js"; import { Innertube } from "youtubei.js";
import { BG } from "bgutils";
import type { konfigLoader } from "../helpers/konfigLoader.ts"; import type { konfigLoader } from "../helpers/konfigLoader.ts";
export type HonoVariables = { export type HonoVariables = {
innertubeClient: Innertube; innertubeClient: Innertube;
konfigStore: Awaited<ReturnType<typeof konfigLoader>>; konfigStore: Awaited<ReturnType<typeof konfigLoader>>;
tokenMinter: BG.WebPoMinter;
}; };

View file

@ -2,8 +2,12 @@ import { Hono } from "hono";
import { routes } from "./routes/index.ts"; import { routes } from "./routes/index.ts";
import { ClientType, Innertube, UniversalCache } from "youtubei.js"; import { ClientType, Innertube, UniversalCache } from "youtubei.js";
import { poTokenGenerate } from "./lib/jobs/potoken.ts"; import { poTokenGenerate } from "./lib/jobs/potoken.ts";
import { USER_AGENT } from "bgutils";
import { konfigLoader } from "./lib/helpers/konfigLoader.ts"; import { konfigLoader } from "./lib/helpers/konfigLoader.ts";
import { retry } from "jsr:@std/async";
import type { HonoVariables } from "./lib/types/HonoVariables.ts"; import type { HonoVariables } from "./lib/types/HonoVariables.ts";
import type { BG } from "bgutils";
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")) {
@ -15,32 +19,15 @@ if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) {
) as string; ) as string;
} }
} }
const args = Deno.args;
const konfigStore = await konfigLoader();
const host = Deno.env.get("HOST") || konfigStore.get("server.host") as string;
const port = Deno.env.get("PORT") || konfigStore.get("server.port") as string;
if (args?.[0] == "healthcheck") {
try {
const response = await fetch(`http://${host}:${port}/healthz`);
if (response.status === 200) {
Deno.exit(0);
} else {
Deno.exit(1);
}
} catch (_) {
Deno.exit(1);
}
}
const { getFetchClient } = await import(getFetchClientLocation); const { getFetchClient } = await import(getFetchClientLocation);
declare module "hono" { declare module "hono" {
interface ContextVariableMap extends HonoVariables {} interface ContextVariableMap extends HonoVariables {}
} }
const app = new Hono(); const app = new Hono();
const konfigStore = await konfigLoader();
let tokenMinter: BG.WebPoMinter;
let innertubeClient: Innertube; let innertubeClient: Innertube;
let innertubeClientFetchPlayer = true; let innertubeClientFetchPlayer = true;
const innertubeClientOauthEnabled = konfigStore.get( const innertubeClientOauthEnabled = konfigStore.get(
@ -73,10 +60,12 @@ if (!innertubeClientOauthEnabled) {
} }
innertubeClient = await Innertube.create({ innertubeClient = await Innertube.create({
enable_session_cache: false,
cache: innertubeClientCache, cache: innertubeClientCache,
retrieve_player: innertubeClientFetchPlayer, retrieve_player: innertubeClientFetchPlayer,
fetch: getFetchClient(konfigStore), fetch: getFetchClient(konfigStore),
cookie: innertubeClientCookies || undefined, cookie: innertubeClientCookies || undefined,
user_agent: USER_AGENT,
}); });
export const innertubeEmbeddedClient = await Innertube.create({ export const innertubeEmbeddedClient = await Innertube.create({
@ -93,24 +82,31 @@ console.log("[INFO] po_token refresh interval set to", poTokenRefreshInterval);
if (!innertubeClientOauthEnabled) { if (!innertubeClientOauthEnabled) {
if (innertubeClientJobPoTokenEnabled) { if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate( ({ innertubeClient, tokenMinter } = await retry(
innertubeClient, poTokenGenerate.bind(
konfigStore, poTokenGenerate,
innertubeClientCache as UniversalCache, innertubeClient,
); konfigStore,
innertubeClientCache as UniversalCache,
),
{ minTimeout: 1_000, maxTimeout: 60_000, multiplier: 5, jitter: 0 },
));
} }
setInterval( setInterval(
async () => { async () => {
if (innertubeClientJobPoTokenEnabled) { if (innertubeClientJobPoTokenEnabled) {
innertubeClient = await poTokenGenerate( ({ innertubeClient, tokenMinter } = await poTokenGenerate(
innertubeClient, innertubeClient,
konfigStore, konfigStore,
innertubeClientCache, innertubeClientCache,
); ));
} else { } else {
innertubeClient = await Innertube.create({ innertubeClient = await Innertube.create({
enable_session_cache: false,
cache: innertubeClientCache, cache: innertubeClientCache,
fetch: getFetchClient(konfigStore),
retrieve_player: innertubeClientFetchPlayer, retrieve_player: innertubeClientFetchPlayer,
user_agent: USER_AGENT,
}); });
} }
}, },
@ -140,6 +136,7 @@ if (!innertubeClientOauthEnabled) {
app.use("*", async (c, next) => { app.use("*", async (c, next) => {
c.set("innertubeClient", innertubeClient); c.set("innertubeClient", innertubeClient);
c.set("tokenMinter", tokenMinter);
c.set("konfigStore", konfigStore); c.set("konfigStore", konfigStore);
await next(); await next();
}); });
@ -147,6 +144,7 @@ app.use("*", async (c, next) => {
routes(app, konfigStore); routes(app, konfigStore);
Deno.serve({ Deno.serve({
port: Number(port), port: Number(Deno.env.get("PORT")) ||
hostname: host konfigStore.get("server.port") as number,
hostname: Deno.env.get("HOST") || konfigStore.get("server.host") as string,
}, app.fetch); }, app.fetch);

View file

@ -6,6 +6,7 @@ import { bearerAuth } from "hono/bearer-auth";
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 invidiousCaptionsApi from "./invidious_routes/captions.ts";
import videoPlaybackProxy from "./videoPlaybackProxy.ts"; import videoPlaybackProxy from "./videoPlaybackProxy.ts";
import metrics from "metrics"; import metrics from "metrics";
import health from "./health.ts"; import health from "./health.ts";
@ -37,6 +38,8 @@ export const routes = (
app.route("/videoplayback", videoPlaybackProxy); app.route("/videoplayback", videoPlaybackProxy);
} }
app.route("/metrics", metrics); app.route("/metrics", metrics);
app.route("/api/v1/captions", invidiousCaptionsApi);
app.route("/videoplayback", videoPlaybackProxy);
app.route("/healthz", health); app.route("/healthz", health);
app.route("/info", info); app.route("/info", info);

View file

@ -0,0 +1,95 @@
import { Hono } from "hono";
import type { HonoVariables } from "../../lib/types/HonoVariables.ts";
import { verifyRequest } from "../../lib/helpers/verifyRequest.ts";
import {
youtubePlayerParsing,
youtubeVideoInfo,
} from "../../lib/helpers/youtubePlayerHandling.ts";
import type { CaptionTrackData } from "youtubei.js/PlayerCaptionsTracklist";
import { handleTranscripts } from "../../lib/helpers/youtubeTranscriptsHandling.ts";
import { HTTPException } from "hono/http-exception";
interface AvailableCaption {
label: string;
languageCode: string;
url: string;
}
const captionsHandler = new Hono<{ Variables: HonoVariables }>();
captionsHandler.get("/:videoId", async (c) => {
const { videoId } = c.req.param();
const konfigStore = c.get("konfigStore");
const check = c.req.query("check");
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 innertubeClient = c.get("innertubeClient");
const youtubePlayerResponseJson = await youtubePlayerParsing({
innertubeClient,
videoId,
konfigStore,
tokenMinter: c.get("tokenMinter"),
});
const videoInfo = youtubeVideoInfo(
innertubeClient,
youtubePlayerResponseJson,
);
const captionsTrackArray = videoInfo.captions?.caption_tracks;
if (captionsTrackArray == undefined) throw new HTTPException(404);
const label = c.req.query("label");
const lang = c.req.query("lang");
// Show all available captions when a specific one is not selected
if (label == undefined && lang == undefined) {
const invidiousAvailableCaptionsArr: AvailableCaption[] = [];
for (const caption_track of captionsTrackArray) {
invidiousAvailableCaptionsArr.push({
label: caption_track.name.text || "",
languageCode: caption_track.language_code,
url: `/api/v1/captions/${videoId}?label=${
encodeURIComponent(caption_track.name.text || "")
}`,
});
}
return c.json({ captions: invidiousAvailableCaptionsArr });
}
// Extract selected caption
let filterSelected: CaptionTrackData[];
if (lang) {
filterSelected = captionsTrackArray.filter((c: CaptionTrackData) =>
c.language_code === lang
);
} else {
filterSelected = captionsTrackArray.filter((c: CaptionTrackData) =>
c.name.text === label
);
}
if (filterSelected.length == 0) throw new HTTPException(404);
c.header("Content-Type", "text/vtt; charset=UTF-8");
return c.body(
await handleTranscripts(innertubeClient, videoId, filterSelected[0]),
);
});
export default captionsHandler;

View file

@ -36,11 +36,12 @@ dashManifest.get("/:videoId", async (c) => {
} }
} }
const youtubePlayerResponseJson = await youtubePlayerParsing( const youtubePlayerResponseJson = await youtubePlayerParsing({
innertubeClient, innertubeClient,
videoId, videoId,
konfigStore, konfigStore,
); tokenMinter: c.get("tokenMinter"),
});
const videoInfo = youtubeVideoInfo( const videoInfo = youtubeVideoInfo(
innertubeClient, innertubeClient,
youtubePlayerResponseJson, youtubePlayerResponseJson,
@ -132,7 +133,7 @@ dashManifest.get("/:videoId", async (c) => {
captions, captions,
undefined, undefined,
); );
return c.body(dashFile.replaceAll("&amp;", "&")); return c.body(dashFile);
} }
}); });

View file

@ -20,7 +20,7 @@ latestVersion.get("/", async (c) => {
} }
const innertubeClient = c.get("innertubeClient"); const innertubeClient = c.get("innertubeClient");
const konfigStore = await c.get("konfigStore"); const konfigStore = c.get("konfigStore");
if (konfigStore.get("server.verify_requests") && check == undefined) { if (konfigStore.get("server.verify_requests") && check == undefined) {
throw new HTTPException(400, { throw new HTTPException(400, {
@ -34,11 +34,12 @@ latestVersion.get("/", async (c) => {
} }
} }
const youtubePlayerResponseJson = await youtubePlayerParsing( const youtubePlayerResponseJson = await youtubePlayerParsing({
innertubeClient, innertubeClient,
id, videoId: id,
konfigStore, konfigStore,
); tokenMinter: c.get("tokenMinter"),
});
const videoInfo = youtubeVideoInfo( const videoInfo = youtubeVideoInfo(
innertubeClient, innertubeClient,
youtubePlayerResponseJson, youtubePlayerResponseJson,

View file

@ -33,8 +33,6 @@ videoPlaybackProxy.get("/", async (c) => {
({ host, c: client, expire } = c.req.query()); ({ host, c: client, expire } = c.req.query());
} }
const rangeHeader = c.req.header("range") as string | undefined;
if (host == undefined || !/[\w-]+.googlevideo.com/.test(host)) { if (host == undefined || !/[\w-]+.googlevideo.com/.test(host)) {
throw new HTTPException(400, { throw new HTTPException(400, {
res: new Response("Host query string do not match or undefined."), res: new Response("Host query string do not match or undefined."),
@ -59,10 +57,12 @@ videoPlaybackProxy.get("/", async (c) => {
} }
queryParams.delete("host"); queryParams.delete("host");
if (rangeHeader) { const rangeHeader = c.req.header("range");
const requestBytes = rangeHeader ? rangeHeader.split("=")[1] : null;
if (requestBytes) {
queryParams.append( queryParams.append(
"range", "range",
rangeHeader.split("=")[1], requestBytes,
); );
} }
@ -109,7 +109,7 @@ videoPlaybackProxy.get("/", async (c) => {
); );
} }
const headersForResponse = { const headersForResponse: Record<string, string> = {
"content-length": googlevideoResponse.headers.get("content-length") || "content-length": googlevideoResponse.headers.get("content-length") ||
"", "",
"access-control-allow-origin": "*", "access-control-allow-origin": "*",
@ -119,8 +119,14 @@ videoPlaybackProxy.get("/", async (c) => {
"last-modified": googlevideoResponse.headers.get("last-modified") || "", "last-modified": googlevideoResponse.headers.get("last-modified") || "",
}; };
let responseStatus = googlevideoResponse.status;
if (requestBytes && responseStatus == 200) {
responseStatus = 206;
headersForResponse["content-range"] = `bytes ${requestBytes}/*`;
}
return new Response(googlevideoResponse.body, { return new Response(googlevideoResponse.body, {
status: googlevideoResponse.status, status: responseStatus,
statusText: googlevideoResponse.statusText, statusText: googlevideoResponse.statusText,
headers: headersForResponse, headers: headersForResponse,
}); });

View file

@ -47,17 +47,14 @@ player.post("/player", async (c) => {
const innertubeClient = c.get("innertubeClient"); const innertubeClient = c.get("innertubeClient");
const konfigStore = c.get("konfigStore"); const konfigStore = c.get("konfigStore");
if (jsonReq.videoId) { if (jsonReq.videoId) {
const yt = await youtubePlayerParsing( return c.json(
innertubeClient, await youtubePlayerParsing({
jsonReq.videoId, innertubeClient,
konfigStore, videoId: jsonReq.videoId,
konfigStore,
tokenMinter: c.get("tokenMinter"),
}),
); );
errors.forEach((error) => {
if (error.check(yt)) {
error.action();
}
});
return c.json(yt);
} }
}); });