diff --git a/deno.json b/deno.json index 2b319e7..6948021 100644 --- a/deno.json +++ b/deno.json @@ -7,6 +7,7 @@ "hono": "jsr:@hono/hono@^4.6.5", "hono/logger": "jsr:@hono/hono@^4.6.5/logger", "hono/bearer-auth": "jsr:@hono/hono@^4.6.5/bearer-auth", + "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/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", diff --git a/src/lib/helpers/youtubePlayerHandling.ts b/src/lib/helpers/youtubePlayerHandling.ts index 64f4b10..7dd6fbe 100644 --- a/src/lib/helpers/youtubePlayerHandling.ts +++ b/src/lib/helpers/youtubePlayerHandling.ts @@ -2,6 +2,7 @@ import { ApiResponse, Innertube, YT } from "youtubei.js"; import { generateRandomString } from "youtubei.js/Utils"; import { compress, decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts"; import { Store } from "@willsoto/node-konfig-core"; +import { cachedEntries } from "../../routes/index.ts"; let youtubePlayerReqLocation = "youtubePlayerReq"; if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) { if (Deno.env.has("DENO_COMPILED")) { @@ -134,6 +135,7 @@ export const youtubePlayerParsing = async ( expireIn: 1000 * 60 * 60, }, ); + cachedEntries.inc() })(); } diff --git a/src/lib/jobs/potoken.ts b/src/lib/jobs/potoken.ts index f91119c..92e3ca3 100644 --- a/src/lib/jobs/potoken.ts +++ b/src/lib/jobs/potoken.ts @@ -3,6 +3,7 @@ import type { BgConfig } from "bgutils"; import { JSDOM } from "jsdom"; import { Innertube, UniversalCache } from "youtubei.js"; import { Store } from "@willsoto/node-konfig-core"; +import { poTokenFail, externalTokenGeneratorFail } from "../../routes/index.ts"; let getFetchClientLocation = "getFetchClient"; if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) { if (Deno.env.has("DENO_COMPILED")) { @@ -70,6 +71,7 @@ export const poTokenGenerate = async ( e, ); console.log("poTokenGenerate: Using built-in token generator"); + externalTokenGeneratorFail.inc() } } @@ -100,6 +102,7 @@ export const poTokenGenerate = async ( const bgChallenge = await BG.Challenge.create(bgConfig); if (!bgChallenge) { + poTokenFail.inc() throw new Error("Could not get challenge"); } @@ -108,7 +111,10 @@ export const poTokenGenerate = async ( if (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({ program: bgChallenge.program, diff --git a/src/routes/index.ts b/src/routes/index.ts index e481268..bfc02d4 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,11 +2,58 @@ import { Hono } from "hono"; import { logger } from "hono/logger"; import { Store } from "@willsoto/node-konfig-core"; import { bearerAuth } from "hono/bearer-auth"; +import { Registry, Counter } from "prom-client" import youtubeApiPlayer from "./youtube_api_routes/player.ts"; import invidiousRouteLatestVersion from "./invidious_routes/latestVersion.ts"; import invidiousRouteDashManifest from "./invidious_routes/dashManifest.ts"; import videoPlaybackProxy from "./videoPlaybackProxy.ts"; +import metrics from "./metrics.ts"; + +const METRICS_PREFIX = "invidious_companion_"; +export const register = new Registry(); + +export const externalTokenGeneratorFail = new Counter({ + name: `${METRICS_PREFIX}externaltokengenerator_fail`, + help: 'TODO', + registers: [register] + }); + +export const cachedEntries = new Counter({ + name: `${METRICS_PREFIX}cached_entries`, + help: 'TODO', + 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 routes = (app: Hono, konfigStore: Store>) => { app.use("*", logger()); @@ -22,4 +69,5 @@ export const routes = (app: Hono, konfigStore: Store>) = app.route("/latest_version", invidiousRouteLatestVersion); app.route("/api/manifest/dash/id", invidiousRouteDashManifest); app.route("/videoplayback", videoPlaybackProxy); + app.route("/metrics", metrics) }; diff --git a/src/routes/metrics.ts b/src/routes/metrics.ts new file mode 100644 index 0000000..deed017 --- /dev/null +++ b/src/routes/metrics.ts @@ -0,0 +1,12 @@ +import { Hono } from "hono"; +import { register } from "./index.ts"; + +const metrics = new Hono(); + +metrics.get("/", async () => { + return new Response(await register.metrics(), { + headers: { "Content-Type": "text/plain" }, + }); +}); + +export default metrics; diff --git a/src/routes/youtube_api_routes/player.ts b/src/routes/youtube_api_routes/player.ts index 1c9fc23..78da8fc 100644 --- a/src/routes/youtube_api_routes/player.ts +++ b/src/routes/youtube_api_routes/player.ts @@ -3,9 +3,33 @@ import { youtubePlayerParsing } from "../../lib/helpers/youtubePlayerHandling.ts import { HonoVariables } from "../../lib/types/HonoVariables.ts"; import { Innertube } from "youtubei.js"; import { Store } from "@willsoto/node-konfig-core"; +import { reasonBot, subreasonProtectCommunity, videoRestricted, videoUnavailable } from "../index.ts"; const player = new Hono<{ Variables: HonoVariables }>(); +const errors = [ + { + // @ts-ignore: Property 'playabilityStatus' does not exist on type 'object' + check: (yt: object) => yt.playabilityStatus?.reason?.includes("Sign in to confirm you’re not a bot"), + action: () => reasonBot.inc(), + }, + { + // @ts-ignore: Property 'playabilityStatus' does not exist on type 'object' + check: (yt: object) => yt.playabilityStatus?.errorScreen?.playerErrorMessageRenderer?.subreason?.runs?.[0]?.text?.includes("This helps protect our community"), + action: () => subreasonProtectCommunity.inc(), + }, + { + // @ts-ignore: Property 'playabilityStatus' does not exist on type 'object' + check: (yt: object) => yt.playabilityStatus?.errorScreen?.playerErrorMessageRenderer?.subreason?.runs?.[0]?.text?.includes("Video unavailable"), + action: () => videoUnavailable.inc(), + }, + { + // @ts-ignore: Property 'playabilityStatus' does not exist on type 'object' + check: (yt: object) => yt.playabilityStatus?.reason?.includes("This video is restricted"), + action: () => videoRestricted.inc(), + }, +]; + player.post("/player", async (c) => { const jsonReq = await c.req.json(); const innertubeClient = await c.get("innertubeClient") as Innertube; @@ -14,9 +38,13 @@ player.post("/player", async (c) => { Record >; if (jsonReq.videoId) { - return c.json( - await youtubePlayerParsing(innertubeClient, jsonReq.videoId, konfigStore) - ); + const yt = await youtubePlayerParsing(innertubeClient, jsonReq.videoId, konfigStore) + errors.forEach((error) => { + if (error.check(yt)) { + error.action() + } + }) + return c.json(yt); } });