diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index b0d2447..bc2283f 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -709,14 +709,17 @@ function parsePlayStreamBody( /** * Bouw play.viaplay.com URL uit templated stream-href (content-API). - * Strip `{?deviceId,...}` en vul device-params + bestaande query. + * RFC 6570 `{?deviceId,...}` → `?` (niet strippen: anders blijft `byguid&deviceKey=` over). */ export function expandViaplayStreamHref( hrefRaw: string, creds: ViaplayAccountCreds, opts: { deviceKey: string } ): string { - const cleaned = hrefRaw.replace(/\{\?[^}]+\}/g, ""); + const cleaned = hrefRaw + .replace(/\{\?[^}]+\}/g, "?") + .replace(/\?&/, "?") + .replace(/\?{2,}/g, "?"); const u = new URL(cleaned); u.searchParams.set("deviceId", creds.deviceId || randomUUID()); u.searchParams.set("deviceName", "web"); @@ -854,22 +857,50 @@ export async function playByGuid( `play-byguid mislukt (${res.status})`; throw new Error(msg); } - const played = parsePlayStreamBody(body, guid); + let played = parsePlayStreamBody(body, guid); if (!played.title && pageTitle) played.title = pageTitle; - // Fallback-promo herkennen - const title = (played.title || "").toLowerCase(); - if (/viaplay\s*presents|don'?t\s*go|viaplay\s*documentary/i.test(title)) { - throw new Error( - `Viaplay gaf fallback-stream (“${played.title}”) — check tokens/producturl` - ); - } - if (played.mpdUrl && /live-dash|play-live|tve_/i.test(played.mpdUrl)) { - throw new Error("Viaplay gaf live-stream i.p.v. VOD voor deze clip"); - } - // Clips horen op vod-cdn - if (played.mpdUrl && !/vod-/i.test(played.mpdUrl)) { - throw new Error(`Onverwachte VOD-MPD host: ${played.mpdUrl.slice(0, 80)}`); + // Kapotte stream-href of ontbrekende producturl → promo-docu. + // Dan één keer opnieuw met expliciete HAR-query i.p.v. hard falen. + const titleLc = (played.title || "").toLowerCase(); + const looksLikePromo = /viaplay\s*presents|don'?t\s*go|viaplay\s*documentary/i.test( + titleLc + ); + if (looksLikePromo && streamHref) { + let productUrlFinal = productUrl; + try { + const pu = new URL(productUrl); + if (!pu.searchParams.has("partial")) pu.searchParams.set("partial", "true"); + productUrlFinal = pu.toString(); + } catch { + /* keep */ + } + const returnUrl = + (opts.returnUrl && String(opts.returnUrl).trim()) || + `https://content.viaplay.com/${opts.deviceKey}/sport`; + const sectionPath = (opts.sectionPath && String(opts.sectionPath).trim()) || "/sport"; + const u = new URL("https://play.viaplay.com/api/stream/byguid"); + u.searchParams.set("deviceId", creds.deviceId || randomUUID()); + u.searchParams.set("deviceName", "web"); + u.searchParams.set("deviceType", "pc"); + u.searchParams.set("userAgent", VIAPLAY_USER_AGENT); + u.searchParams.set("deviceKey", opts.deviceKey); + u.searchParams.set("guid", guid); + u.searchParams.set("cse", "true"); + u.searchParams.set("producturl", productUrlFinal); + u.searchParams.set("returnurl", returnUrl); + u.searchParams.set("sectionPath", sectionPath); + u.searchParams.set("defaultAvailabilityContext", "svod"); + if (creds.profileId) u.searchParams.set("profileId", creds.profileId); + const retry = await fetchJson(u.toString(), { + headers: authHeaders(creds), + timeoutMs: opts.timeoutMs, + }); + const retryBody = asRecord(retry.json); + if (retry.status === 200 && retryBody) { + played = parsePlayStreamBody(retryBody, guid); + if (!played.title && pageTitle) played.title = pageTitle; + } } return played; }