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; : 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:{…}`). * Electron/Playwright geeft cookies decoded (`s:j:{…}`).
* In de Cookie-header moeten die percent-encoded — anders parst Viaplay * In de Cookie-header moeten die percent-encoded — anders parst Viaplay
@ -541,6 +594,40 @@ export async function fetchSportHighlights(
return extractProducts(res.json); 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). */ /** Catalog-item van de Viaplay sport-sectie (plank/rail). */
export type ViaplaySportRailItem = { export type ViaplaySportRailItem = {
guid: string; guid: string;
@ -671,9 +758,9 @@ function productToRailItem(node: Record<string, unknown>): ViaplaySportRailItem
const title = productTitle(content, guid); const title = productTitle(content, guid);
const subtitle = const subtitle =
String( contentSynopsis(content) ||
content?.originalTitle ?? content?.synopsis ?? content?.description ?? "" viaplayPlainText(content?.originalTitle) ||
).trim() || null; viaplayPlainText(content?.subtitle);
rememberStreamHref(guid, href); rememberStreamHref(guid, href);
@ -819,8 +906,11 @@ function extractProducts(json: unknown): ViaplayEpgProduct[] {
const sport = sportFromPageUrl(page); const sport = sportFromPageUrl(page);
out.push({ out.push({
guid, guid,
title: String(content?.title ?? system?.guid ?? guid).trim(), title: viaplayPlainText(content?.title) || String(system?.guid ?? guid).trim(),
subtitle: String(content?.subtitle ?? content?.description ?? "").trim() || null, subtitle:
contentSynopsis(content) ||
viaplayPlainText(content?.subtitle) ||
viaplayPlainText(content?.originalTitle),
startMs, startMs,
endMs, endMs,
imageUrl: pickImage(content), imageUrl: pickImage(content),

View file

@ -1,11 +1,14 @@
import type { ScheduleEventPublic } from "../viewer/schedule-events"; import type { ScheduleEventPublic } from "../viewer/schedule-events";
import { import {
fetchProductSynopsis,
fetchSportRails, fetchSportRails,
viaplayPlainText,
type ViaplaySportRail, type ViaplaySportRail,
type ViaplaySportRailItem, type ViaplaySportRailItem,
} from "./client"; } from "./client";
import { import {
loadViaplayConfigOnly, loadViaplayConfigOnly,
patchViaplayEventCache,
} from "./settings"; } from "./settings";
import type { ViaplayEventSnapshot } from "./tweaks"; import type { ViaplayEventSnapshot } from "./tweaks";
import { toMediaGuid } from "./accounts"; import { toMediaGuid } from "./accounts";
@ -30,10 +33,12 @@ export function eventToPublic(
nowMs: number nowMs: number
): ScheduleEventPublic { ): ScheduleEventPublic {
const phase = phaseFor(event.startMs, event.endMs, nowMs); const phase = phaseFor(event.startMs, event.endMs, nowMs);
const description = viaplayPlainText(event.subtitle);
return { return {
id: toEventId(event.mediaGuid), id: toEventId(event.mediaGuid),
name: event.title, name: event.title,
description: event.subtitle, description:
description && description !== event.title ? description : null,
provider: "viaplay", provider: "viaplay",
category: event.category || "sport", category: event.category || "sport",
eventType: "live", eventType: "live",
@ -92,8 +97,20 @@ export async function getViaplayScheduleEvent(
e.mediaGuid === `${mediaGuid}-NL` || e.mediaGuid === `${mediaGuid}-NL` ||
e.mediaGuid.replace(/-NL$/i, "") === mediaGuid.replace(/-NL$/i, "") e.mediaGuid.replace(/-NL$/i, "") === mediaGuid.replace(/-NL$/i, "")
); );
if (!event) return null; if (!event || !config) return null;
return eventToPublic(event, nowMs); 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 = { export type ViaplaySportChip = {

View file

@ -54,7 +54,10 @@ function parseEvents(raw: unknown): ViaplayEventSnapshot[] {
guid, guid,
mediaGuid, mediaGuid,
title, 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, category: typeof row.category === "string" ? row.category : null,
sportLabel: typeof row.sportLabel === "string" ? row.sportLabel : null, sportLabel: typeof row.sportLabel === "string" ? row.sportLabel : null,
sportBucket: typeof row.sportBucket === "string" ? row.sportBucket : null, sportBucket: typeof row.sportBucket === "string" ? row.sportBucket : null,