From 97e76c3551bd0b9e85968d017f948e9b09b9e67e Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 29 Sep 2026 01:23:22 +0200 Subject: [PATCH] Show Viaplay event synopsis instead of [object Object] on the detail page. --- apps/master-api/src/viaplay/client.ts | 100 ++++++++++++++++++++++-- apps/master-api/src/viaplay/events.ts | 23 +++++- apps/master-api/src/viaplay/settings.ts | 5 +- 3 files changed, 119 insertions(+), 9 deletions(-) diff --git a/apps/master-api/src/viaplay/client.ts b/apps/master-api/src/viaplay/client.ts index 601bdc5..eaaa1cf 100644 --- a/apps/master-api/src/viaplay/client.ts +++ b/apps/master-api/src/viaplay/client.ts @@ -25,6 +25,59 @@ function asRecord(v: unknown): Record | null { : null; } +/** Viaplay zet synopsis/description vaak als `{ value, formatted }`, niet als string. */ +export function viaplayPlainText(raw: unknown, depth = 0): string | null { + if (raw == null) return null; + if (typeof raw === "number" && Number.isFinite(raw)) return String(raw); + if (typeof raw === "string") { + const t = raw + .replace(/<[^>]+>/g, " ") + .replace(/ /gi, " ") + .replace(/&/gi, "&") + .replace(/\s+/g, " ") + .trim(); + if (!t || /^\[object \w+]$/i.test(t)) return null; + return t; + } + if (depth > 4) return null; + if (Array.isArray(raw)) { + for (const item of raw) { + const t = viaplayPlainText(item, depth + 1); + if (t) return t; + } + return null; + } + const rec = asRecord(raw); + if (!rec) return null; + for (const key of [ + "value", + "formatted", + "text", + "synopsis", + "long", + "short", + "description", + "nl", + "en", + ]) { + const t = viaplayPlainText(rec[key], depth + 1); + if (t) return t; + } + return null; +} + +export function contentSynopsis(content: Record | null): string | null { + if (!content) return null; + return ( + viaplayPlainText(content.synopsis) || + viaplayPlainText(content.description) || + viaplayPlainText(content.shortDescription) || + viaplayPlainText(content.longDescription) || + viaplayPlainText(content.editorial) || + null + ); +} + /** * Electron/Playwright geeft cookies decoded (`s:j:{…}`). * In de Cookie-header moeten die percent-encoded — anders parst Viaplay @@ -541,6 +594,40 @@ export async function fetchSportHighlights( return extractProducts(res.json); } +/** Productpagina → echte synopsis (niet `String(object)`). */ +export async function fetchProductSynopsis( + pageUrl: string, + timeoutMs: number +): Promise { + const raw = String(pageUrl || "").trim(); + if (!raw) return null; + let url = raw; + try { + const u = new URL(raw); + u.searchParams.set("partial", "true"); + u.searchParams.set("language", u.searchParams.get("language") || "nl"); + url = u.toString(); + } catch { + return null; + } + const res = await fetchJson(url, { + headers: { + Accept: "application/json", + "User-Agent": VIAPLAY_USER_AGENT, + Referer: "https://viaplay.com/", + }, + timeoutMs, + }); + if (res.status !== 200) return null; + const root = asRecord(res.json); + const embedded = asRecord(root?._embedded); + const product = + asRecord(embedded?.["viaplay:product"]) || + asRecord(root?.product) || + root; + return contentSynopsis(asRecord(product?.content)); +} + /** Catalog-item van de Viaplay sport-sectie (plank/rail). */ export type ViaplaySportRailItem = { guid: string; @@ -671,9 +758,9 @@ function productToRailItem(node: Record): ViaplaySportRailItem const title = productTitle(content, guid); const subtitle = - String( - content?.originalTitle ?? content?.synopsis ?? content?.description ?? "" - ).trim() || null; + contentSynopsis(content) || + viaplayPlainText(content?.originalTitle) || + viaplayPlainText(content?.subtitle); rememberStreamHref(guid, href); @@ -819,8 +906,11 @@ function extractProducts(json: unknown): ViaplayEpgProduct[] { const sport = sportFromPageUrl(page); out.push({ guid, - title: String(content?.title ?? system?.guid ?? guid).trim(), - subtitle: String(content?.subtitle ?? content?.description ?? "").trim() || null, + title: viaplayPlainText(content?.title) || String(system?.guid ?? guid).trim(), + subtitle: + contentSynopsis(content) || + viaplayPlainText(content?.subtitle) || + viaplayPlainText(content?.originalTitle), startMs, endMs, imageUrl: pickImage(content), diff --git a/apps/master-api/src/viaplay/events.ts b/apps/master-api/src/viaplay/events.ts index 2dbd89a..f68812d 100644 --- a/apps/master-api/src/viaplay/events.ts +++ b/apps/master-api/src/viaplay/events.ts @@ -1,11 +1,14 @@ import type { ScheduleEventPublic } from "../viewer/schedule-events"; import { + fetchProductSynopsis, fetchSportRails, + viaplayPlainText, type ViaplaySportRail, type ViaplaySportRailItem, } from "./client"; import { loadViaplayConfigOnly, + patchViaplayEventCache, } from "./settings"; import type { ViaplayEventSnapshot } from "./tweaks"; import { toMediaGuid } from "./accounts"; @@ -30,10 +33,12 @@ export function eventToPublic( nowMs: number ): ScheduleEventPublic { const phase = phaseFor(event.startMs, event.endMs, nowMs); + const description = viaplayPlainText(event.subtitle); return { id: toEventId(event.mediaGuid), name: event.title, - description: event.subtitle, + description: + description && description !== event.title ? description : null, provider: "viaplay", category: event.category || "sport", eventType: "live", @@ -92,8 +97,20 @@ export async function getViaplayScheduleEvent( e.mediaGuid === `${mediaGuid}-NL` || e.mediaGuid.replace(/-NL$/i, "") === mediaGuid.replace(/-NL$/i, "") ); - if (!event) return null; - return eventToPublic(event, nowMs); + if (!event || !config) return null; + const pub = eventToPublic(event, nowMs); + if (pub.description || !event.pageUrl) return pub; + try { + const text = await fetchProductSynopsis( + event.pageUrl, + config.httpTimeoutMs || 15_000 + ); + if (!text || text === event.title) return pub; + void patchViaplayEventCache(event.mediaGuid, { subtitle: text }); + return { ...pub, description: text }; + } catch { + return pub; + } } export type ViaplaySportChip = { diff --git a/apps/master-api/src/viaplay/settings.ts b/apps/master-api/src/viaplay/settings.ts index 0bfe57d..9fbc667 100644 --- a/apps/master-api/src/viaplay/settings.ts +++ b/apps/master-api/src/viaplay/settings.ts @@ -54,7 +54,10 @@ function parseEvents(raw: unknown): ViaplayEventSnapshot[] { guid, mediaGuid, title, - subtitle: typeof row.subtitle === "string" ? row.subtitle : null, + subtitle: + typeof row.subtitle === "string" && !/^\[object \w+]$/i.test(row.subtitle.trim()) + ? row.subtitle + : null, category: typeof row.category === "string" ? row.category : null, sportLabel: typeof row.sportLabel === "string" ? row.sportLabel : null, sportBucket: typeof row.sportBucket === "string" ? row.sportBucket : null,