diff --git a/apps/master-api/src/odido/client.ts b/apps/master-api/src/odido/client.ts index d41dff5..aef23d8 100644 --- a/apps/master-api/src/odido/client.ts +++ b/apps/master-api/src/odido/client.ts @@ -1,13 +1,51 @@ import { createHash } from "node:crypto"; const BASE_URL = "https://tv.odido.nl"; + +/** + * Browser-achtige UA + Client Hints. Odido weigert/ruikt kale Node-fetch of + * requests zonder User-Agent sneller; houd dit in sync met desktop Chrome. + * Override via Scripts → Odido tweaks `userAgent` indien nodig. + */ export const ODIDO_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + - "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; + "(KHTML, like Gecko) Chrome/140.0.7339.208 Safari/537.36"; + +const ODIDO_SEC_CH_UA = + '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"'; + /** Fallback als er geen username is; liever deviceIdForUsername(). */ const DEFAULT_DEVICE_ID = "MzkzNjIwMTU1NjAxMDNydA=="; const DEFAULT_SESSION_CACHE_MS = 20 * 60_000; +let runtimeUserAgent = ODIDO_USER_AGENT; + +/** Laat sync/play de (optioneel getweakte) UA zetten vóór requests. */ +export function setOdidoUserAgent(ua: string | null | undefined) { + const next = (ua ?? "").trim(); + runtimeUserAgent = next || ODIDO_USER_AGENT; +} + +export function getOdidoUserAgent(): string { + return runtimeUserAgent || ODIDO_USER_AGENT; +} + +function odidoBrowserHeaders(extra?: Record): Record { + const ua = getOdidoUserAgent(); + return { + Accept: "application/json, text/plain, */*", + "Accept-Language": "nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7", + "User-Agent": ua, + "sec-ch-ua": ODIDO_SEC_CH_UA, + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + ...extra, + }; +} + /** Stabiele, per-account device-id zodat sessies elkaar niet wegtrappen. */ export function deviceIdForUsername(username: string): string { const hash = createHash("sha256") @@ -87,14 +125,13 @@ async function post( ): Promise<{ body: Record; cookies: OdidoCookies }> { const response = await fetch(`${BASE_URL}${path}?from=${from}`, { method: "POST", - headers: { + headers: odidoBrowserHeaders({ "Content-Type": "text/plain;charset=UTF-8", Origin: BASE_URL, Referer: `${BASE_URL}/inloggen/index.html`, - "User-Agent": ODIDO_USER_AGENT, ...(cookies?.CSRFSESSION ? { X_CSRFToken: cookies.CSRFSESSION } : {}), ...(cookies ? { Cookie: cookieHeader(cookies) } : {}), - }, + }), body: JSON.stringify(data), signal: AbortSignal.timeout(path.includes("QueryAllChannel") ? 60_000 : 30_000), }); @@ -260,7 +297,7 @@ export async function playChannel( contentId: [body.contentID, body.contentId, trigger?.contentID, trigger?.contentId] .find((value): value is string => typeof value === "string" && !!value) ?? null, cookies: refreshedCookies, - userAgent: ODIDO_USER_AGENT, + userAgent: getOdidoUserAgent(), }; } @@ -269,10 +306,12 @@ export async function getPssh( cookies: OdidoCookies = {} ): Promise { const response = await fetch(mpdUrl, { - headers: { - "User-Agent": ODIDO_USER_AGENT, + headers: odidoBrowserHeaders({ + Accept: "*/*", + Referer: `${BASE_URL}/`, + Origin: BASE_URL, ...(Object.keys(cookies).length ? { Cookie: cookieHeader(cookies) } : {}), - }, + }), signal: AbortSignal.timeout(30_000), }); if (!response.ok) throw new Error(`Odido MPD ophalen mislukt (${response.status})`); diff --git a/apps/master-api/src/odido/play.ts b/apps/master-api/src/odido/play.ts index 2489099..b81fad8 100644 --- a/apps/master-api/src/odido/play.ts +++ b/apps/master-api/src/odido/play.ts @@ -2,9 +2,9 @@ import { prisma } from "../database/client"; import { encryptSecret } from "../security/crypto"; import { extractOdidoKeys } from "./cdm"; import { + getOdidoUserAgent, getPssh, login, - ODIDO_USER_AGENT, playChannel, type OdidoCookies, type OdidoPlayResult, @@ -50,7 +50,11 @@ async function probeMpd(url: string, timeoutMs: number, cookies?: OdidoCookies): const response = await fetch(url, { method: "GET", headers: { - "User-Agent": ODIDO_USER_AGENT, + Accept: "*/*", + "Accept-Language": "nl-NL,nl;q=0.9,en;q=0.8", + "User-Agent": getOdidoUserAgent(), + Referer: "https://tv.odido.nl/", + Origin: "https://tv.odido.nl", ...(cookies && Object.keys(cookies).length ? { Cookie: Object.entries(cookies)