Show Viaplay event synopsis instead of [object Object] on the detail page.

This commit is contained in:
Jos Vooges | STH 2026-09-29 01:23:22 +02:00
parent 84657acbc9
commit 97e76c3551
3 changed files with 119 additions and 9 deletions

View file

@ -25,6 +25,59 @@ function asRecord(v: unknown): Record<string, unknown> | 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(/&nbsp;/gi, " ")
.replace(/&amp;/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<string, unknown> | 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<string | null> {
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<string, unknown>): 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),

View file

@ -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 = {

View file

@ -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,