diff --git a/apps/master-api/src/admin/routes.ts b/apps/master-api/src/admin/routes.ts index 5b00562..91fa0cd 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-v6", + resolver: "viaplay-byguid-har-v7", }; } catch (err) { return { ok: false, guid, error: err instanceof Error ? err.message : String(err), - resolver: "viaplay-byguid-har-v6", + resolver: "viaplay-byguid-har-v7", }; } } diff --git a/apps/master-api/src/agent/routes.ts b/apps/master-api/src/agent/routes.ts index e6459ae..b9d9f1e 100644 --- a/apps/master-api/src/agent/routes.ts +++ b/apps/master-api/src/agent/routes.ts @@ -227,7 +227,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) { mediaPath, keyCount: played.keys.length, promo, - resolver: "viaplay-byguid-har-v6", + resolver: "viaplay-byguid-har-v7", lastVodPlay: getLastVodPlay(), }; } catch (err) { @@ -235,7 +235,7 @@ export function registerAgentRoutes(app: FastifyInstance, config: Config) { ok: false, guid, error: err instanceof Error ? err.message : String(err), - resolver: "viaplay-byguid-har-v6", + resolver: "viaplay-byguid-har-v7", lastVodPlay: getLastVodPlay(), }; } diff --git a/apps/master-api/src/app.ts b/apps/master-api/src/app.ts index fcacdef..e78423c 100644 --- a/apps/master-api/src/app.ts +++ b/apps/master-api/src/app.ts @@ -106,7 +106,7 @@ async function main() { return { status: "ok", service: "master-api", - version: "1.0.3-vod-assert-v6b", + version: "1.0.4-dontgo-detect-v7", lastVodPlay: getLastVodPlay(), }; }); diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index 5c76faf..3cbc0c7 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -907,10 +907,42 @@ function playBodyGuid(body: Record): string | null { return typeof system?.guid === "string" ? system.guid.trim() : null; } +/** Bekende Don't go soft-fail CDN-asset (titel/guid kloppen, media niet). */ +const VIAPLAY_DONT_GO_MEDIA_RE = /\/7c4cd64\/88000100-1778484912807\.ism\b/i; + +function cseMediaGuid(body: Record): string | null { + try { + const cse = asRecord(body.cseReporting); + const link = asRecord(asRecord(cse?.link)?.reportingUrl); + const href = typeof link?.href === "string" ? link.href : ""; + const m = href.match(/\/viaplay\/([^/$]+)/); + if (!m?.[1]) return null; + const payload = JSON.parse( + Buffer.from(m[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString() + ) as { mediaGuid?: string }; + return typeof payload.mediaGuid === "string" ? payload.mediaGuid : null; + } catch { + return null; + } +} + +function mediaHrefFromPlayBody(body: Record): string | null { + const links = asRecord(body._links); + for (const key of [ + "viaplay:media", + "viaplay:playlist", + "viaplay:encryptedPlaylist", + ] as const) { + const href = asRecord(links?.[key])?.href; + if (typeof href === "string" && href.trim()) return href.trim(); + } + return null; +} + /** - * HAR-success: guid + titel + product-link + productType. - * Soft-fail “Don't go” komt vaak als 200 met lege titel / ontbrekende guid — - * dat mag nooit door (anders plakt pageTitle over de promo-MPD). + * HAR-success: guid + titel + product-link + echte media (geen Don't go soft-fail). + * Soft-fail: 200 met juiste product-metadata maar placeholder-MPD “Don't go” + * (vaak bij max devices / geen stream-recht). */ function assertVodPlayMatchesGuid( body: Record, @@ -931,6 +963,20 @@ function assertVodPlayMatchesGuid( if (isPromoVodTitle(played.title)) { throw new Error(`Viaplay gaf promo-stream (“${played.title}”)`); } + + const mediaHref = mediaHrefFromPlayBody(body) || played.mpdUrl; + if (VIAPLAY_DONT_GO_MEDIA_RE.test(mediaHref)) { + throw new Error( + "Viaplay soft-fail: Don't go-placeholder i.p.v. stream (vaak te veel apparaten — log uit op viaplay.com → Apparaten beheren)" + ); + } + const streamMediaGuid = cseMediaGuid(body); + if (streamMediaGuid === "unknown") { + throw new Error( + "Viaplay soft-fail: mediaGuid=unknown (geen echte stream — check apparaatlimiet of abonnement)" + ); + } + const links = asRecord(body._links); const productHref = asRecord(links?.["viaplay:product"])?.href; if (typeof productHref !== "string" || !productHref.trim()) { @@ -958,13 +1004,11 @@ function assertVodPlayMatchesGuid( } const durationMs = Number(body.duration); if (Number.isFinite(durationMs) && durationMs > 0) { - // Ultrashorte bumper/slate i.p.v. echte clip (highlights mogen ~30–60s zijn) if (wantsClip && durationMs < 15_000) { throw new Error( `Viaplay gaf te korte stream (${Math.round(durationMs / 1000)}s) — promo-slate?` ); } - // Feature-lengte i.p.v. clip if (wantsClip && durationMs > 2.5 * 60 * 60 * 1000) { throw new Error( `Viaplay gaf te lange stream (${Math.round(durationMs / 60000)} min)` diff --git a/apps/master-api/src/viaplay/play.ts b/apps/master-api/src/viaplay/play.ts index 52fa6c8..70e00a3 100644 --- a/apps/master-api/src/viaplay/play.ts +++ b/apps/master-api/src/viaplay/play.ts @@ -44,7 +44,7 @@ function recordVodPlay( partial: Omit ) { lastVodPlay = { ...partial, at: new Date().toISOString(), - resolver: "viaplay-byguid-har-v6", + resolver: "viaplay-byguid-har-v7", }; } diff --git a/apps/master-api/src/viaplay/settings.ts b/apps/master-api/src/viaplay/settings.ts index b12005b..841289e 100644 --- a/apps/master-api/src/viaplay/settings.ts +++ b/apps/master-api/src/viaplay/settings.ts @@ -454,11 +454,18 @@ export async function loadViaplaySettings(sessionSecret: string): Promise a.enabled !== false) .map((a) => { try { - return decryptAccount(a, sessionSecret); + const creds = decryptAccount(a, sessionSecret); + // Stabiel deviceId bewaren — random per decrypt verbrandt Viaplay's 5-device limiet. + if (!a.deviceId && creds.deviceId) { + a.deviceId = creds.deviceId; + deviceIdsAssigned = true; + } + return creds; } catch { return null; } @@ -475,6 +482,13 @@ export async function loadViaplaySettings(sessionSecret: string): Promise { try { const u = new URL(played.streamUrl);