diff --git a/apps/master-api/src/admin/routes.ts b/apps/master-api/src/admin/routes.ts index 91fa0cd..ebb9a86 100644 --- a/apps/master-api/src/admin/routes.ts +++ b/apps/master-api/src/admin/routes.ts @@ -1928,14 +1928,14 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config) streamUrl: played.streamUrl, keyCount: played.keys.length, promo, - resolver: "viaplay-byguid-har-v7", + resolver: "viaplay-byguid-v9", }; } catch (err) { return { ok: false, guid, error: err instanceof Error ? err.message : String(err), - resolver: "viaplay-byguid-har-v7", + resolver: "viaplay-byguid-v9", }; } } diff --git a/apps/master-api/src/agent/routes.ts b/apps/master-api/src/agent/routes.ts index b9d9f1e..1eeb3ee 100644 --- a/apps/master-api/src/agent/routes.ts +++ b/apps/master-api/src/agent/routes.ts @@ -163,6 +163,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) { accessToken?: string; profileId?: string; persistentLogin?: string; + boid?: string; }; const { upsertViaplayAccountByEmail } = await import("../viaplay/settings"); return upsertViaplayAccountByEmail(config.SESSION_SECRET, { @@ -172,6 +173,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) { accessToken: String(body.accessToken ?? ""), profileId: body.profileId, persistentLogin: body.persistentLogin, + boid: body.boid, }); }); diff --git a/apps/master-api/src/viaplay/accounts.ts b/apps/master-api/src/viaplay/accounts.ts index 58e845a..564508c 100644 --- a/apps/master-api/src/viaplay/accounts.ts +++ b/apps/master-api/src/viaplay/accounts.ts @@ -11,6 +11,8 @@ export type ViaplayAccountStored = { sessionEnc?: string; accessTokenEnc?: string; profileId?: string; + /** Chrome __Secure-Viaplay-Boid — livescript stuurt hem mee. */ + boid?: string; deviceId?: string; /** Optioneel — voor latere tray/login. */ passwordEnc?: string; @@ -52,6 +54,7 @@ export type ViaplayAccountCreds = { session: string | null; accessToken: string | null; profileId: string | null; + boid: string | null; deviceId: string; password: string | null; persistentLogin: string | null; @@ -106,6 +109,7 @@ export function decryptAccount( ? decryptSecret(account.accessTokenEnc, sessionSecret) : null, profileId: account.profileId?.trim() || null, + boid: account.boid?.trim() || null, deviceId: account.deviceId?.trim() || randomUUID(), password: account.passwordEnc ? decryptSecret(account.passwordEnc, sessionSecret) @@ -127,6 +131,7 @@ export function createAccountStored( session?: string; accessToken?: string; profileId?: string; + boid?: string; deviceId?: string; password?: string; persistentLogin?: string; @@ -152,6 +157,7 @@ export function createAccountStored( ? encryptSecret(accessToken, sessionSecret) : undefined, profileId: input.profileId?.trim() || undefined, + boid: input.boid?.trim() || undefined, deviceId: input.deviceId?.trim() || randomUUID(), passwordEnc: password ? encryptSecret(password, sessionSecret) : undefined, persistentLoginEnc: input.persistentLogin?.trim() @@ -178,6 +184,7 @@ export function parseAccountsJson(raw: unknown): ViaplayAccountStored[] { sessionEnc: String(row.sessionEnc ?? "").trim() || undefined, accessTokenEnc: String(row.accessTokenEnc ?? "").trim() || undefined, profileId: String(row.profileId ?? "").trim() || undefined, + boid: String(row.boid ?? "").trim() || undefined, deviceId: String(row.deviceId ?? "").trim() || undefined, passwordEnc: String(row.passwordEnc ?? "").trim() || undefined, persistentLoginEnc: String(row.persistentLoginEnc ?? "").trim() || undefined, diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index 4c37ff5..1239f59 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -2,8 +2,9 @@ import { randomUUID } from "node:crypto"; import type { ViaplayAccountCreds } from "./accounts"; import { toMediaGuid } from "./accounts"; +/** Zelfde UA als het werkende livescript (viaplay3.py). Edge/153 + sec-ch-ua gaf Don't go. */ export const VIAPLAY_USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0"; + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"; export type ViaplayPlayResult = { mediaGuid: string; @@ -13,6 +14,8 @@ export type ViaplayPlayResult = { channelName: string | null; stationId: string | null; title: string | null; + /** Product-duration uit play-JSON (niet de MPD — Don't go liegt hier). */ + durationMs: number | null; }; function asRecord(v: unknown): Record | null { @@ -21,20 +24,44 @@ function asRecord(v: unknown): Record | null { : null; } -/** Netscape/browser-cookies blijven encoded; decoded JSON in Cookie-header = Don't go. */ +/** + * Electron/Playwright geeft cookies decoded (`s:j:{…}`). + * In de Cookie-header moeten die percent-encoded — anders parst Viaplay + * de session niet en geeft 200 + Don't go. + * Al encoded (`s%3Aj%3A%7B`) laten we met rust. + */ function cookieValue(raw: string): string { const v = raw.trim(); if (!v) return v; - if (/%[0-9A-Fa-f]{2}/.test(v)) return v; - if (/[{}"\s]/.test(v)) return encodeURIComponent(v); + if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) { + return cookieValue(v.slice(1, -1)); + } + if (/[{}"\s,;\\]/.test(v)) return encodeURIComponent(v); return v; } +function parseSetCookieJar(setCookies: string[]): Record { + const jar: Record = {}; + for (const sc of setCookies) { + const m = sc.match(/^([^=]+)=([^;]*)/); + if (!m) continue; + let v = m[2] ?? ""; + if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) { + v = v.slice(1, -1); + } + jar[m[1]!] = v; + } + return jar; +} + function cookieHeader(creds: ViaplayAccountCreds): string { const parts: string[] = []; if (creds.session) parts.push(`session=${cookieValue(creds.session)}`); if (creds.accessToken) parts.push(`accessToken=${creds.accessToken.trim()}`); if (creds.profileId) parts.push(`viaplay_profileId=${creds.profileId.trim()}`); + if (creds.boid) { + parts.push(`__Secure-Viaplay-Boid=${creds.boid.trim()}`); + } if (creds.persistentLogin) { parts.push(`persistentLogin=${cookieValue(creds.persistentLogin)}`); } @@ -62,18 +89,9 @@ export function authHeaders(creds: ViaplayAccountCreds): Record export function playHeaders(creds: ViaplayAccountCreds): Record { return { Accept: "*/*", - "Accept-Language": - "nl,en;q=0.9,en-GB;q=0.8,en-US;q=0.7,es-ES;q=0.6,es;q=0.5,nl-NL;q=0.4", Origin: "https://viaplay.com", Referer: "https://viaplay.com/", "User-Agent": VIAPLAY_USER_AGENT, - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-site", - "sec-ch-ua": - '"Microsoft Edge";v="153", "Not_A Brand";v="8", "Chromium";v="153"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"Windows"', Cookie: cookieHeader(creds), }; } @@ -107,20 +125,16 @@ export async function refreshViaplaySession( Origin: "https://viaplay.com", Referer: "https://viaplay.com/", "User-Agent": VIAPLAY_USER_AGENT, - Cookie: `persistentLogin=${pl}`, + Cookie: `persistentLogin=${cookieValue(pl)}`, }, signal: AbortSignal.timeout(opts.timeoutMs), }); - const jar: Record = {}; const rawSet = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : []; - for (const sc of rawSet) { - const m = sc.match(/^([^=]+)=([^;]*)/); - if (m) jar[m[1]!] = m[2]!; - } + const jar = parseSetCookieJar(rawSet); let body: Record | null = null; try { @@ -147,6 +161,7 @@ export async function refreshViaplaySession( session, accessToken, persistentLogin: nextPl, + boid: jar["__Secure-Viaplay-Boid"] || creds.boid, }; } @@ -793,73 +808,78 @@ function extractProducts(json: unknown): ViaplayEpgProduct[] { /** * Don't go is Viaplay's soft-fail (200 + promo-VOD) i.p.v. 403. - * Titel/guid van het event kloppen vaak wél; alleen de media is fout. + * Titel/guid/body.duration van het gevraagde product kloppen wél; + * alleen de media (en de echte MPD-duur) is de promo-docu (~PT43M36S). */ const VIAPLAY_DONT_GO_MEDIA_RE = /88000100-1778484912807|\b7c4cd64\b|\/dont[-_]?go/i; +/** Bekende mediaPresentationDuration van de Don't go-docu. */ +const DONT_GO_MPD_DURATION_MS = 2_616_000; export function isDontGoPlaceholder(mpdUrl: string | null | undefined): boolean { return !!mpdUrl && VIAPLAY_DONT_GO_MEDIA_RE.test(mpdUrl); } +export function parseIsoDurationMs(iso: string): number | null { + const t = String(iso || "").trim(); + if (!t) return null; + const m = t.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?$/i); + if (!m) return null; + const h = Number(m[1] || 0); + const min = Number(m[2] || 0); + const s = Number(m[3] || 0); + if (![h, min, s].some((n) => Number.isFinite(n) && n > 0)) return null; + return Math.round(((h * 60 + min) * 60 + s) * 1000); +} + +export function parseMpdDurationMs(xml: string): number | null { + const raw = + xml.match(/\bmediaPresentationDuration=["']([^"']+)["']/i)?.[1] || + xml.match(/]*\bduration=["']([^"']+)["']/i)?.[1]; + return raw ? parseIsoDurationMs(raw) : null; +} + +export function mpdLooksLikeDontGo( + xml: string, + expectedDurationMs?: number | null +): boolean { + if (/don'?t\s*go|viaplay\s*presents/i.test(xml)) return true; + const mpdMs = parseMpdDurationMs(xml); + if (mpdMs == null) return false; + if (Math.abs(mpdMs - DONT_GO_MPD_DURATION_MS) <= 4_000) return true; + const expected = Number(expectedDurationMs); + if (Number.isFinite(expected) && expected > 30_000) { + const ratio = mpdMs / expected; + if (ratio < 0.55 || ratio > 1.8) return true; + } + return false; +} + export function isLiveDashMpd(mpdUrl: string | null | undefined): boolean { return !!mpdUrl && /live-dash/i.test(mpdUrl); } -function assertPlayNotSoftFail( - played: ViaplayPlayResult, - opts?: { requireLiveDash?: boolean } -): void { +function assertPlayNotSoftFail(played: ViaplayPlayResult): void { if (isPromoVodTitle(played.title)) { throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`); } if (isDontGoPlaceholder(played.mpdUrl)) { throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream"); } - if (/vod-dash/i.test(played.mpdUrl) && !played.licenseUrl) { - throw new Error("Viaplay gaf Don't go-VOD zonder licentie"); - } - if (!played.licenseUrl) { - throw new Error("Viaplay play-response mist Widevine-licentie (Don't go-fallback)"); - } - if (opts?.requireLiveDash && !isLiveDashMpd(played.mpdUrl)) { + if (!isLiveDashMpd(played.mpdUrl)) { throw new Error( "Viaplay gaf geen live-dash stream (Don't go/VOD-fallback — sessie of rechten)" ); } -} - -function sportContentUrls( - pageUrl: string | null | undefined, - deviceKey: string, - mediaGuid: string -): { productUrl: string; contextualUrl: string; contextualv2Url: string } { - const guidPath = mediaGuid.replace(/-NL$/i, "").toLowerCase(); - let clean = ""; - const raw = (pageUrl || "").trim(); - if (raw) { - try { - const u = new URL(raw); - u.search = ""; - clean = u.toString().replace(/\/$/, ""); - } catch { - clean = ""; - } + if (!played.licenseUrl) { + throw new Error("Viaplay play-response mist Widevine-licentie (Don't go-fallback)"); } - if (!clean) { - clean = `https://content.viaplay.com/${deviceKey}/sport/${guidPath}`; - } - return { - productUrl: `${clean}?partial=true`, - contextualUrl: `${clean}?contextual=true`, - contextualv2Url: `${clean}?contextualV2=true&productType=sport`, - }; } function buildPlayLiveUrl( creds: ViaplayAccountCreds, mediaGuid: string, - opts: { deviceKey: string; pageUrl?: string | null; harStyle: boolean } + opts: { deviceKey: string } ): string { const u = new URL("https://play-live.viaplay.com/api/stream/bymediaguid"); u.searchParams.set("deviceId", creds.deviceId || randomUUID()); @@ -869,26 +889,6 @@ function buildPlayLiveUrl( u.searchParams.set("deviceKey", opts.deviceKey); u.searchParams.set("mediaGuid", mediaGuid); u.searchParams.set("nocc", "autoplayer"); - if (creds.profileId) u.searchParams.set("profileId", creds.profileId); - if (!opts.harStyle) return u.toString(); - - const pages = sportContentUrls(opts.pageUrl, opts.deviceKey, mediaGuid); - const returnUrl = new URL(`https://content.viaplay.com/${opts.deviceKey}`); - returnUrl.searchParams.set("language", "nl"); - if (creds.profileId) returnUrl.searchParams.set("profileId", creds.profileId); - u.searchParams.set("cse", "true"); - u.searchParams.set( - "splitIOFlagIds", - "all_sports_beyondLiveBeacon_on,web_player_pause_ads_on" - ); - u.searchParams.set("tcString", VIAPLAY_TC_STRING); - u.searchParams.set("returnurl", returnUrl.toString()); - u.searchParams.set("producturl", pages.productUrl); - u.searchParams.set("contextualurl", pages.contextualUrl); - u.searchParams.set("contextualv2url", pages.contextualv2Url); - u.searchParams.set("sectionPath", "/sport"); - u.searchParams.set("defaultAvailabilityContext", "svod"); - u.searchParams.set("win10edge", "true"); return u.toString(); } @@ -899,45 +899,27 @@ export async function playByMediaGuid( deviceKey: string; timeoutMs: number; pageUrl?: string | null; - /** Terugkijken: echte VOD-MPD toestaan, Don't go niet. */ allowVod?: boolean; } ): Promise { + void opts.pageUrl; + void opts.allowVod; const mediaGuid = toMediaGuid(mediaGuidRaw); - const urls = [ - buildPlayLiveUrl(creds, mediaGuid, { - deviceKey: opts.deviceKey, - harStyle: false, - }), - buildPlayLiveUrl(creds, mediaGuid, { - deviceKey: opts.deviceKey, - pageUrl: opts.pageUrl, - harStyle: true, - }), - ]; - - let lastError: Error | null = null; - for (const playUrl of urls) { - try { - const res = await fetchJson(playUrl, { - headers: playHeaders(creds), - timeoutMs: opts.timeoutMs, - }); - const body = asRecord(res.json); - if (res.status !== 200 || !body) { - const msg = - (typeof body?.message === "string" && body.message) || - `play-live mislukt (${res.status})`; - throw new Error(msg); - } - const played = parsePlayStreamBody(body, mediaGuid); - assertPlayNotSoftFail(played, { requireLiveDash: !opts.allowVod }); - return played; - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - } + const playUrl = buildPlayLiveUrl(creds, mediaGuid, { deviceKey: opts.deviceKey }); + const res = await fetchJson(playUrl, { + headers: playHeaders(creds), + timeoutMs: opts.timeoutMs, + }); + const body = asRecord(res.json); + if (res.status !== 200 || !body) { + const msg = + (typeof body?.message === "string" && body.message) || + `play-live mislukt (${res.status})`; + throw new Error(msg); } - throw lastError || new Error("Viaplay play-live mislukt"); + const played = parsePlayStreamBody(body, mediaGuid); + assertPlayNotSoftFail(played); + return played; } function parsePlayStreamBody( @@ -952,6 +934,7 @@ function parsePlayStreamBody( if (!mpdUrl) throw new Error("Geen viaplay:media in play-response"); const product = asRecord(body.product); const channel = asRecord(body.channel) || asRecord(asRecord(body.streamingSession)?.channel); + const durationRaw = Number(body.duration); return { mediaGuid: id, mpdUrl, @@ -963,6 +946,8 @@ function parsePlayStreamBody( typeof asRecord(product?.content)?.title === "string" ? String(asRecord(product!.content)!.title) : null, + durationMs: + Number.isFinite(durationRaw) && durationRaw > 0 ? durationRaw : null, }; } @@ -1080,9 +1065,6 @@ function assertVodPlayMatchesGuid( "Viaplay gaf Don't go-placeholder i.p.v. de gevraagde stream" ); } - if (/vod-dash/i.test(played.mpdUrl) && !played.licenseUrl) { - throw new Error("Viaplay gaf Don't go-VOD zonder licentie"); - } const links = asRecord(body._links); const productHref = asRecord(links?.["viaplay:product"])?.href; @@ -1254,7 +1236,6 @@ export async function playByGuid( contextualv2FromStreamHref(listingHref) || contextualv2FromStreamHref(productHref); - // Eerst exacte HAR-query (betrouwbaar), daarna eventuele stream-hrefs. const playUrls: string[] = []; const seen = new Set(); const pushUrl = (url: string) => { @@ -1262,6 +1243,14 @@ export async function playByGuid( seen.add(url); playUrls.push(url); }; + const simple = new URL("https://play.viaplay.com/api/stream/byguid"); + simple.searchParams.set("deviceId", creds.deviceId || randomUUID()); + simple.searchParams.set("deviceName", "web"); + simple.searchParams.set("deviceType", "pc"); + simple.searchParams.set("userAgent", VIAPLAY_USER_AGENT); + simple.searchParams.set("deviceKey", opts.deviceKey); + simple.searchParams.set("guid", guid); + pushUrl(simple.toString()); pushUrl( buildHarByguidUrl(creds, guid, { deviceKey: opts.deviceKey, @@ -1297,8 +1286,29 @@ export async function playByGuid( durationMs: pageDurationMs, title: pageTitle, }); - // Alleen aanvullen als play al een echte (niet-promo) titel heeft — nooit - // pageTitle over een lege promo-response plakken (dat verborgt Don't go). + const expectedMs = pageDurationMs || played.durationMs; + let mpdChecked = false; + try { + const mpd = await fetchMpdXml(played.mpdUrl, opts.timeoutMs); + if (mpd.status === 200 && /]/i.test(mpd.xml)) { + mpdChecked = true; + if ( + isDontGoPlaceholder(mpd.url) || + mpdLooksLikeDontGo(mpd.xml, expectedMs) + ) { + throw new Error( + "Viaplay MPD is Don't go-placeholder (duur/media mismatch)" + ); + } + } + } catch (err) { + if (err instanceof Error && /Don't go/i.test(err.message)) throw err; + } + if (!mpdChecked && /vod-dash/i.test(played.mpdUrl) && !played.licenseUrl) { + throw new Error( + "VOD zonder licentie en MPD niet te valideren — Don't go-risico" + ); + } if (!played.title?.trim() && pageTitle && !isPromoVodTitle(pageTitle)) { played.title = pageTitle; } diff --git a/apps/master-api/src/viaplay/play.ts b/apps/master-api/src/viaplay/play.ts index 65d3678..2751809 100644 --- a/apps/master-api/src/viaplay/play.ts +++ b/apps/master-api/src/viaplay/play.ts @@ -16,6 +16,7 @@ import { fetchViaplayBootstrapPssh, isDontGoPlaceholder, isLiveDashMpd, + mpdLooksLikeDontGo, VIAPLAY_USER_AGENT, } from "./client"; import { @@ -51,7 +52,7 @@ function recordVodPlay( partial: Omit ) { lastVodPlay = { ...partial, at: new Date().toISOString(), - resolver: "viaplay-byguid-har-v8", + resolver: "viaplay-byguid-v9", }; } @@ -209,9 +210,15 @@ export async function ensureViaplayEventReady( !!event.mpdUrl && isMpdCacheFresh(event.mpdCachedAt, tweaks.mpdCacheTtlMinutes); - if (cacheFresh && cachedKeys.length && !isDontGoPlaceholder(event.mpdUrl)) { + if ( + cacheFresh && + cachedKeys.length && + event.mpdUrl && + isLiveDashMpd(event.mpdUrl) && + !isDontGoPlaceholder(event.mpdUrl) + ) { return { - streamUrl: event.mpdUrl!, + streamUrl: event.mpdUrl, format: "dash", keys: cachedKeys, fromCache: true, @@ -265,6 +272,7 @@ export async function ensureViaplayEventReady( if ( cacheFresh && event.mpdUrl && + isLiveDashMpd(event.mpdUrl) && !isDontGoPlaceholder(event.mpdUrl) && !(opts?.fetchKeys ?? tweaks.fetchKeysIfMissing) ) { @@ -297,6 +305,7 @@ export async function ensureViaplayEventReady( session: refreshed.session || undefined, accessToken: refreshed.accessToken || undefined, persistentLogin: refreshed.persistentLogin || undefined, + boid: refreshed.boid || undefined, }); } catch { /* continue with in-memory refresh */ @@ -308,7 +317,6 @@ export async function ensureViaplayEventReady( deviceKey: tweaks.deviceKey, timeoutMs: tweaks.httpTimeoutMs, pageUrl: event.pageUrl, - allowVod: ended, }); const kap = isLiveDashMpd(play.mpdUrl) ? await resolveKapMpd( @@ -507,6 +515,7 @@ async function resolveViaplayGuidPlayInner( session: refreshed.session || undefined, accessToken: refreshed.accessToken || undefined, persistentLogin: refreshed.persistentLogin || undefined, + boid: refreshed.boid || undefined, }); } catch { /* play mag doorgaan met in-memory refresh */ @@ -530,11 +539,19 @@ async function resolveViaplayGuidPlayInner( try { const mpd = await fetchMpdXml(play.mpdUrl, tweaks.httpTimeoutMs); if (mpd.status === 200 && /]/i.test(mpd.xml)) { + if ( + isDontGoPlaceholder(mpd.url) || + mpdLooksLikeDontGo(mpd.xml, play.durationMs) + ) { + throw new Error( + "Viaplay MPD is Don't go-placeholder (duur/media mismatch)" + ); + } pssh = extractPsshFromMpd(mpd.xml); mpdUrl = mpd.url; } - } catch { - /* speel base MPD */ + } catch (err) { + if (err instanceof Error && /Don't go/i.test(err.message)) throw err; } // Clear VOD (HAR: geen license) — nooit stale ClearKey van eerdere plays plakken. diff --git a/apps/master-api/src/viaplay/settings.ts b/apps/master-api/src/viaplay/settings.ts index 841289e..ca02a8d 100644 --- a/apps/master-api/src/viaplay/settings.ts +++ b/apps/master-api/src/viaplay/settings.ts @@ -202,6 +202,7 @@ export async function addViaplayAccount( session?: string; accessToken?: string; profileId?: string; + boid?: string; password?: string; persistentLogin?: string; } @@ -233,6 +234,7 @@ export async function upsertViaplayAccountByEmail( session: string; accessToken: string; profileId?: string; + boid?: string; persistentLogin?: string; } ) { @@ -252,6 +254,7 @@ export async function upsertViaplayAccountByEmail( session: input.session, accessToken: input.accessToken, profileId: input.profileId, + boid: input.boid, persistentLogin: input.persistentLogin, enabled: true, }); @@ -262,6 +265,7 @@ export async function upsertViaplayAccountByEmail( session: input.session, accessToken: input.accessToken, profileId: input.profileId, + boid: input.boid, persistentLogin: input.persistentLogin, }); } @@ -276,6 +280,7 @@ export async function updateViaplayAccount( session?: string; accessToken?: string; profileId?: string; + boid?: string; password?: string; persistentLogin?: string; clearCookies?: boolean; @@ -296,11 +301,13 @@ export async function updateViaplayAccount( let sessionEnc = current.sessionEnc; let accessTokenEnc = current.accessTokenEnc; let profileId = current.profileId; + let boid = current.boid; let persistentLoginEnc = current.persistentLoginEnc; if (input.clearCookies) { sessionEnc = undefined; accessTokenEnc = undefined; profileId = undefined; + boid = undefined; persistentLoginEnc = undefined; } if (input.session?.trim()) { @@ -316,6 +323,9 @@ export async function updateViaplayAccount( if (input.profileId !== undefined) { profileId = input.profileId.trim() || undefined; } + if (input.boid !== undefined) { + boid = input.boid.trim() || undefined; + } if (input.persistentLogin?.trim()) { persistentLoginEnc = encryptSecret(input.persistentLogin.trim(), sessionSecret); } @@ -339,6 +349,7 @@ export async function updateViaplayAccount( sessionEnc, accessTokenEnc, profileId, + boid, persistentLoginEnc, passwordEnc: input.password?.trim() ? encryptSecret(input.password.trim(), sessionSecret) diff --git a/apps/master-api/src/viewer/routes.ts b/apps/master-api/src/viewer/routes.ts index f7a0af2..51d9254 100644 --- a/apps/master-api/src/viewer/routes.ts +++ b/apps/master-api/src/viewer/routes.ts @@ -1096,7 +1096,7 @@ export function registerViewerRoutes( fallbackFormat: null, live: false, /** Deploy/debug: bevestigt VOD-resolver revisie na Dokploy. */ - resolver: "viaplay-byguid-har-v8", + resolver: "viaplay-byguid-v9", mediaPath: (() => { try { const u = new URL(played.streamUrl); diff --git a/apps/token-tray/src/jobs.ts b/apps/token-tray/src/jobs.ts index b728930..b7e9947 100644 --- a/apps/token-tray/src/jobs.ts +++ b/apps/token-tray/src/jobs.ts @@ -53,6 +53,7 @@ async function pushViaplay( accessToken: string; profileId: string | null; persistentLogin: string | null; + boid?: string | null; } ) { const res = await fetch(`${cfg.apiUrl}/api/v1/agent/viaplay/tokens`, { @@ -68,6 +69,7 @@ async function pushViaplay( accessToken: tokens.accessToken, profileId: tokens.profileId || undefined, persistentLogin: tokens.persistentLogin || undefined, + boid: tokens.boid || undefined, }), }); const data = (await res.json().catch(() => ({}))) as { error?: { message?: string } }; diff --git a/apps/token-tray/src/login/viaplay.ts b/apps/token-tray/src/login/viaplay.ts index cad877c..c497e07 100644 --- a/apps/token-tray/src/login/viaplay.ts +++ b/apps/token-tray/src/login/viaplay.ts @@ -15,6 +15,7 @@ export type ViaplayLoginResult = { accessToken: string; profileId: string | null; persistentLogin: string | null; + boid: string | null; }; function profileDir(accountId: string): string { @@ -96,8 +97,9 @@ async function readCookies(context: BrowserContext): Promise ({}))) as {