Improve Ziggo art matching for highlights and strip live thumbs.
Samenvattingen like Moto3 Austria now match via series aliases (Austria/Oostenrijk, Moto3) without strict air-time overlap; unmatched Racing rows drop live channel thumbs so football-on-air never stays as card art.
This commit is contained in:
parent
5a990ae346
commit
5db9658e84
2 changed files with 309 additions and 50 deletions
|
|
@ -1,11 +1,14 @@
|
|||
import {
|
||||
artUrlsFromMergedId,
|
||||
isLiveChannelThumb,
|
||||
isReplayOrHighlightTitle,
|
||||
isZiggoSportChannel,
|
||||
needsZiggoArtEnrichment,
|
||||
normalizeTitleTokens,
|
||||
pickBestZiggoMatch,
|
||||
segmentKeysForRange,
|
||||
seriesAnchorTokens,
|
||||
stripLiveThumbs,
|
||||
titleOverlapScore,
|
||||
timesOverlap,
|
||||
type ZiggoEpgEvent,
|
||||
|
|
@ -22,6 +25,7 @@ assert(!isLiveChannelThumb("https://staticqbr-prod-nl.gnp.cloud.ziggogo.tv/image
|
|||
assert(isZiggoSportChannel("Ziggo Sport 1"), "zs1");
|
||||
assert(isZiggoSportChannel("Ziggo Sport"), "zs");
|
||||
assert(!isZiggoSportChannel("ESPN"), "espn");
|
||||
assert(isReplayOrHighlightTitle("Moto3 BWIN Grand Prix of Austria (samenvatting)"), "samenvatting");
|
||||
|
||||
assert(
|
||||
needsZiggoArtEnrichment({
|
||||
|
|
@ -37,7 +41,9 @@ assert(arts?.landscape.includes("p24692584_bd_h8_aa.jpg"), "landscape url");
|
|||
assert(arts?.portrait.includes("p24692584_b_v12_aa.jpg"), "portrait url");
|
||||
|
||||
assert(titleOverlapScore("GT World Challenge: Nederland Race 2", "GT World Challenge Hoogtepunten Nederland: Race 2") >= 0.5, "overlap");
|
||||
assert(normalizeTitleTokens("Ziggo Sport: GT World Challenge").has("world"), "tokens");
|
||||
assert(normalizeTitleTokens("Ziggo Sport: GT World Challenge").has("gtworld"), "gtworld alias");
|
||||
assert(normalizeTitleTokens("Grand Prix of Austria").has("oostenrijk"), "austria→oostenrijk");
|
||||
assert(seriesAnchorTokens("Moto3 BWIN Grand Prix of Austria").has("moto3"), "anchor moto3");
|
||||
|
||||
const start = Date.parse("2026-09-20T23:00:00Z");
|
||||
const end = Date.parse("2026-09-21T00:00:00Z");
|
||||
|
|
@ -69,6 +75,57 @@ const hit = pickBestZiggoMatch(candidates, {
|
|||
});
|
||||
assert(hit?.id === "a", "pick GT not football");
|
||||
|
||||
// Moto3 samenvatting days later than live session — must still resolve via loose match
|
||||
const motoCands: ZiggoEpgEvent[] = [
|
||||
{
|
||||
id: "m3",
|
||||
channelId: "NL_000094_019321",
|
||||
title: "Moto3",
|
||||
seriesName: "GP van Oostenrijk: Kwalificatie 2",
|
||||
startTime: 1789815900,
|
||||
endTime: 1789817700,
|
||||
mergedId: "15289756|nl",
|
||||
},
|
||||
{
|
||||
id: "foot",
|
||||
channelId: "NL_000094_019321",
|
||||
title: "Eredivisie samenvatting",
|
||||
seriesName: "Ajax - PSV",
|
||||
startTime: 1789972200,
|
||||
endTime: 1789974000,
|
||||
mergedId: "999|nl",
|
||||
},
|
||||
];
|
||||
const motoHit = pickBestZiggoMatch(motoCands, {
|
||||
title: "Moto3 BWIN Grand Prix of Austria (samenvatting)",
|
||||
startMs: Date.parse("2026-09-21T06:30:00Z"),
|
||||
endMs: Date.parse("2026-09-21T07:00:00Z"),
|
||||
});
|
||||
assert(motoHit?.id === "m3", `moto3 loose match got ${motoHit?.id}`);
|
||||
assert(motoHit?.mergedId === "15289756|nl", "moto3 mergedId");
|
||||
|
||||
const stripped = stripLiveThumbs(
|
||||
{
|
||||
id: "x",
|
||||
name: "Moto3",
|
||||
channel: "Ziggo Sport 1",
|
||||
network: "Ziggo Sport 1",
|
||||
start: "2026-09-21T06:30:00Z",
|
||||
stop: "2026-09-21T07:00:00Z",
|
||||
imageLandscape: "https://zst1-akm-cdn-triple.akamaized.net/live/zs1/thumb.jpg?scale=640",
|
||||
imageBackdrop: null,
|
||||
imagePortrait: null,
|
||||
logoUrl: null,
|
||||
images: [
|
||||
"https://go.ziggosporttotaal.nl/assets/channels/light/ziggo_sport_1.png",
|
||||
"https://zst1-akm-cdn-triple.akamaized.net/live/zs1/thumb.jpg?scale=640",
|
||||
],
|
||||
},
|
||||
"https://go.ziggosporttotaal.nl/assets/channels/light/ziggo_sport_1.png"
|
||||
);
|
||||
assert(!isLiveChannelThumb(stripped.imageLandscape), "stripped landscape");
|
||||
assert(!(stripped.images ?? []).some(isLiveChannelThumb), "no live thumbs left");
|
||||
|
||||
const keys = segmentKeysForRange(start, end);
|
||||
assert(keys.length >= 1, "segment keys");
|
||||
assert(keys.every((k) => /^\d{14}$/.test(k)), "key format");
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
* Enrich Ziggo Sport events with real Gracenote art from Ziggo GO EPG
|
||||
* (epg-service-lite + image-service), instead of live channel thumbs.
|
||||
*
|
||||
* Match: channel name → Ziggo channelId(s) + time overlap + title tokens.
|
||||
* Match: channel → Ziggo channelId(s) + (time overlap | series family) + title tokens.
|
||||
* Art: ImagesEPG/EventImages/p{mergedId}_bd_h8_aa.jpg
|
||||
* Fallback: strip live thumbs (never leave football-on-air as Racing art).
|
||||
*/
|
||||
|
||||
const CHANNELS_URL =
|
||||
|
|
@ -16,6 +17,7 @@ const IMAGE_BASE =
|
|||
const FETCH_TIMEOUT_MS = 12_000;
|
||||
const CACHE_TTL_MS = 15 * 60_000;
|
||||
const SIX_H_MS = 6 * 60 * 60 * 1000;
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export type ZiggoEpgEvent = {
|
||||
id: string;
|
||||
|
|
@ -28,11 +30,16 @@ export type ZiggoEpgEvent = {
|
|||
isSportsSportEvent?: boolean;
|
||||
};
|
||||
|
||||
type ChannelRow = { id?: string; name?: string };
|
||||
type ChannelRow = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
logo?: { focused?: string; default?: string };
|
||||
};
|
||||
|
||||
type CacheState = {
|
||||
at: number;
|
||||
byChannelName: Map<string, string[]>; // normalized name → channelIds
|
||||
byChannelName: Map<string, string[]>;
|
||||
logoByChannelId: Map<string, string>;
|
||||
events: ZiggoEpgEvent[];
|
||||
};
|
||||
|
||||
|
|
@ -51,6 +58,17 @@ export function isLiveChannelThumb(url: string | null | undefined): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function isChannelLogoUrl(url: string | null | undefined): boolean {
|
||||
if (!url) return false;
|
||||
const u = url.toLowerCase();
|
||||
return (
|
||||
u.includes("assets/channels") ||
|
||||
u.includes("channellogo") ||
|
||||
u.includes("/imagesepg/eventimages/ziggo_sport") ||
|
||||
/ziggo_sport_\d/i.test(u)
|
||||
);
|
||||
}
|
||||
|
||||
export function needsZiggoArtEnrichment(e: {
|
||||
channel?: string | null;
|
||||
network?: string | null;
|
||||
|
|
@ -60,11 +78,8 @@ export function needsZiggoArtEnrichment(e: {
|
|||
if (!isZiggoSportChannel(e.channel) && !isZiggoSportChannel(e.network)) return false;
|
||||
if (!e.imageLandscape && !(e.images?.length)) return true;
|
||||
if (isLiveChannelThumb(e.imageLandscape)) return true;
|
||||
if ((e.images ?? []).some(isLiveChannelThumb) && isLiveChannelThumb(e.imageLandscape)) return true;
|
||||
// Landscape is only a channel logo → enrich
|
||||
if (e.imageLandscape && /assets\/channels|channelLogo|ziggo_sport_\d/i.test(e.imageLandscape)) {
|
||||
return true;
|
||||
}
|
||||
if ((e.images ?? []).some(isLiveChannelThumb)) return true;
|
||||
if (e.imageLandscape && isChannelLogoUrl(e.imageLandscape)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +96,38 @@ export function normalizeChannelName(name: string | null | undefined): string {
|
|||
.trim();
|
||||
}
|
||||
|
||||
/** Strip channel prefixes / punctuation for fuzzy title match. */
|
||||
export function normalizeTitleTokens(raw: string | null | undefined): Set<string> {
|
||||
let s = String(raw ?? "").toLowerCase();
|
||||
s = s.replace(/^ziggo sport(?:\s*\d+)?\s*:\s*/i, "");
|
||||
s = s.replace(/^espn\d?\s*:\s*/i, "");
|
||||
s = s.replace(/[^\p{L}\p{N}\s]+/gu, " ");
|
||||
const stop = new Set([
|
||||
export function isReplayOrHighlightTitle(title: string): boolean {
|
||||
return /\b(samenvatting|hoogtepunten|highlights?|herhaling|replay|samenvattingen)\b/i.test(
|
||||
title
|
||||
);
|
||||
}
|
||||
|
||||
/** Map NL/EN sports place & series aliases onto shared tokens. */
|
||||
const TOKEN_ALIASES: Record<string, string[]> = {
|
||||
austria: ["oostenrijk", "austria", "redbullring"],
|
||||
oostenrijk: ["oostenrijk", "austria", "redbullring"],
|
||||
austriaans: ["oostenrijk", "austria"],
|
||||
moto3: ["moto3", "moto"],
|
||||
moto2: ["moto2", "moto"],
|
||||
motogp: ["motogp", "moto"],
|
||||
moto: ["moto"],
|
||||
gp: ["gp", "grandprix"],
|
||||
grand: ["gp", "grandprix"],
|
||||
prix: ["gp", "grandprix"],
|
||||
grandprix: ["gp", "grandprix"],
|
||||
bwin: ["bwin"],
|
||||
imsa: ["imsa"],
|
||||
nascar: ["nascar"],
|
||||
indycar: ["indycar", "indy"],
|
||||
gt: ["gt", "gtworld"],
|
||||
world: ["gtworld", "world"],
|
||||
challenge: ["gtworld", "challenge"],
|
||||
zandvoort: ["zandvoort", "nederland"],
|
||||
nederland: ["nederland", "netherlands"],
|
||||
netherlands: ["nederland", "netherlands"],
|
||||
};
|
||||
|
||||
const STOP = new Set([
|
||||
"de",
|
||||
"het",
|
||||
"een",
|
||||
|
|
@ -99,11 +139,32 @@ export function normalizeTitleTokens(raw: string | null | undefined): Set<string
|
|||
"live",
|
||||
"vs",
|
||||
"v",
|
||||
"samenvatting",
|
||||
"hoogtepunten",
|
||||
"highlights",
|
||||
"highlight",
|
||||
"herhaling",
|
||||
"replay",
|
||||
"race",
|
||||
"races",
|
||||
]);
|
||||
|
||||
/** Strip channel prefixes / punctuation; expand aliases for fuzzy match. */
|
||||
export function normalizeTitleTokens(raw: string | null | undefined): Set<string> {
|
||||
let s = String(raw ?? "").toLowerCase();
|
||||
s = s.replace(/^ziggo sport(?:\s*\d+)?\s*:\s*/i, "");
|
||||
s = s.replace(/^espn\s*\d?\s*:\s*/i, "");
|
||||
s = s.replace(/\bmoto\s*([123])\b/g, "moto$1");
|
||||
s = s.replace(/\bgrand\s+prix\b/g, "grandprix");
|
||||
s = s.replace(/\bgt\s*world(?:\s*challenge)?\b/g, "gtworld");
|
||||
s = s.replace(/[^\p{L}\p{N}\s]+/gu, " ");
|
||||
const out = new Set<string>();
|
||||
for (const t of s.split(/\s+/)) {
|
||||
if (t.length < 3 || stop.has(t)) continue;
|
||||
if (t.length < 2 || STOP.has(t)) continue;
|
||||
if (t.length < 3 && !/^moto\d?$|^gp$/.test(t)) continue;
|
||||
out.add(t);
|
||||
const aliases = TOKEN_ALIASES[t];
|
||||
if (aliases) for (const a of aliases) out.add(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -117,6 +178,30 @@ export function titleOverlapScore(a: string, b: string): number {
|
|||
return hit / Math.min(ta.size, tb.size);
|
||||
}
|
||||
|
||||
/** Strong series anchors — required for loose (non-time) matching. */
|
||||
export function seriesAnchorTokens(title: string): Set<string> {
|
||||
const t = normalizeTitleTokens(title);
|
||||
const anchors = [
|
||||
"moto3",
|
||||
"moto2",
|
||||
"motogp",
|
||||
"gtworld",
|
||||
"imsa",
|
||||
"nascar",
|
||||
"indycar",
|
||||
"formule1",
|
||||
"f1",
|
||||
"wrc",
|
||||
"dtm",
|
||||
"superbike",
|
||||
"sbk",
|
||||
];
|
||||
const out = new Set<string>();
|
||||
for (const a of anchors) if (t.has(a)) out.add(a);
|
||||
// also raw moto3 from "Moto 3" category-ish already normalized
|
||||
return out;
|
||||
}
|
||||
|
||||
export function artUrlsFromMergedId(mergedId: string | null | undefined): {
|
||||
landscape: string;
|
||||
portrait: string;
|
||||
|
|
@ -141,30 +226,86 @@ export function timesOverlap(
|
|||
return aStartMs < b1 && aEndMs > b0;
|
||||
}
|
||||
|
||||
function scoreCandidate(
|
||||
hay: string,
|
||||
c: ZiggoEpgEvent,
|
||||
opts: { startMs: number; endMs: number; loose: boolean }
|
||||
): number {
|
||||
const zigTitle = [c.title, c.seriesName].filter(Boolean).join(" ");
|
||||
let score = titleOverlapScore(hay, zigTitle);
|
||||
const nl = hay.toLowerCase();
|
||||
const zl = zigTitle.toLowerCase();
|
||||
|
||||
if (nl.includes("gt world") && zl.includes("gt world")) score += 0.45;
|
||||
if (/\bmoto3\b/i.test(nl) && /\bmoto3\b/i.test(zl)) score += 0.55;
|
||||
if (/\bmoto2\b/i.test(nl) && /\bmoto2\b/i.test(zl)) score += 0.55;
|
||||
if (/\bmotogp\b/i.test(nl) && /\bmotogp\b/i.test(zl)) score += 0.55;
|
||||
if (/\bimsa\b/i.test(nl) && /\bimsa\b/i.test(zl)) score += 0.45;
|
||||
if (/\bnascar\b/i.test(nl) && /\bnascar\b/i.test(zl)) score += 0.45;
|
||||
|
||||
// Place: Austria ↔ Oostenrijk already via aliases; boost if both sides have it
|
||||
const placeA = normalizeTitleTokens(hay);
|
||||
const placeB = normalizeTitleTokens(zigTitle);
|
||||
if (placeA.has("oostenrijk") && placeB.has("oostenrijk")) score += 0.25;
|
||||
|
||||
if (c.isSportsSportEvent) score += 0.05;
|
||||
if (/\bhoogtepunten\b/i.test(zl) && isReplayOrHighlightTitle(hay)) score += 0.1;
|
||||
|
||||
if (opts.loose) {
|
||||
// Prefer closer airings even in loose mode
|
||||
const mid = (opts.startMs + opts.endMs) / 2;
|
||||
const cMid = ((c.startTime + c.endTime) / 2) * 1000;
|
||||
const dt = Math.abs(mid - cMid);
|
||||
if (dt > 4 * DAY_MS) return 0;
|
||||
score += Math.max(0, 0.2 - (dt / (4 * DAY_MS)) * 0.2);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
export function pickBestZiggoMatch(
|
||||
candidates: ZiggoEpgEvent[],
|
||||
opts: { title: string; startMs: number; endMs: number }
|
||||
): ZiggoEpgEvent | null {
|
||||
const hay = opts.title;
|
||||
const highlight = isReplayOrHighlightTitle(hay);
|
||||
const anchors = seriesAnchorTokens(hay);
|
||||
|
||||
// Pass 1: time overlap (wider slack for summaries)
|
||||
const slack = highlight ? 6 * 60 * 60_000 : 45 * 60_000;
|
||||
let best: ZiggoEpgEvent | null = null;
|
||||
let bestScore = 0;
|
||||
const hay = `${opts.title}`;
|
||||
for (const c of candidates) {
|
||||
if (!timesOverlap(opts.startMs, opts.endMs, c.startTime, c.endTime)) continue;
|
||||
const zigTitle = [c.title, c.seriesName].filter(Boolean).join(" ");
|
||||
let score = titleOverlapScore(hay, zigTitle);
|
||||
// Boost exact-ish contains
|
||||
const nl = hay.toLowerCase();
|
||||
const zl = zigTitle.toLowerCase();
|
||||
if (nl.includes("gt world") && zl.includes("gt world")) score += 0.5;
|
||||
if (nl.includes("motogp") && zl.includes("motogp")) score += 0.5;
|
||||
if (c.isSportsSportEvent) score += 0.05;
|
||||
if (!timesOverlap(opts.startMs, opts.endMs, c.startTime, c.endTime, slack)) continue;
|
||||
const score = scoreCandidate(hay, c, { ...opts, loose: false });
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
// Require at least one shared token (score > 0) or strong boost
|
||||
return bestScore >= 0.34 ? best : null;
|
||||
if (best && bestScore >= 0.3) return best;
|
||||
|
||||
// Pass 2: loose series match within ±3 days — for samenvatting/herhaling or strong anchors
|
||||
if (!highlight && anchors.size === 0) return bestScore >= 0.34 ? best : null;
|
||||
|
||||
best = null;
|
||||
bestScore = 0;
|
||||
for (const c of candidates) {
|
||||
const zigTitle = [c.title, c.seriesName].filter(Boolean).join(" ");
|
||||
const cAnchors = seriesAnchorTokens(zigTitle);
|
||||
// Require shared series anchor when loose
|
||||
let sharedAnchor = false;
|
||||
for (const a of anchors) if (cAnchors.has(a)) sharedAnchor = true;
|
||||
if (anchors.size && !sharedAnchor) continue;
|
||||
|
||||
const score = scoreCandidate(hay, c, { ...opts, loose: true });
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
// Higher bar without time lock
|
||||
return bestScore >= 0.45 ? best : null;
|
||||
}
|
||||
|
||||
function segmentKeyUtc(ms: number): string {
|
||||
|
|
@ -213,9 +354,10 @@ async function loadZiggoIndex(fromMs: number, toMs: number): Promise<CacheState
|
|||
if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache;
|
||||
|
||||
const channels = await fetchJson<ChannelRow[]>(CHANNELS_URL);
|
||||
if (!channels?.length) return cache; // keep stale if any
|
||||
if (!channels?.length) return cache;
|
||||
|
||||
const byChannelName = new Map<string, string[]>();
|
||||
const logoByChannelId = new Map<string, string>();
|
||||
for (const ch of channels) {
|
||||
const id = String(ch.id ?? "").trim();
|
||||
const name = normalizeChannelName(ch.name);
|
||||
|
|
@ -223,9 +365,12 @@ async function loadZiggoIndex(fromMs: number, toMs: number): Promise<CacheState
|
|||
const list = byChannelName.get(name) ?? [];
|
||||
if (!list.includes(id)) list.push(id);
|
||||
byChannelName.set(name, list);
|
||||
const logo = ch.logo?.focused || ch.logo?.default;
|
||||
if (logo) logoByChannelId.set(id, logo);
|
||||
}
|
||||
|
||||
const keys = segmentKeysForRange(fromMs, toMs);
|
||||
// Extra ±3 days so samenvatting can resolve earlier race airings
|
||||
const keys = segmentKeysForRange(fromMs - 3 * DAY_MS, toMs + DAY_MS);
|
||||
const events: ZiggoEpgEvent[] = [];
|
||||
const seen = new Set<string>();
|
||||
await Promise.all(
|
||||
|
|
@ -263,7 +408,7 @@ async function loadZiggoIndex(fromMs: number, toMs: number): Promise<CacheState
|
|||
})
|
||||
);
|
||||
|
||||
cache = { at: Date.now(), byChannelName, events };
|
||||
cache = { at: Date.now(), byChannelName, logoByChannelId, events };
|
||||
return cache;
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +417,6 @@ function channelIdsFor(index: CacheState, channel: string | null | undefined): s
|
|||
if (!n) return [];
|
||||
const exact = index.byChannelName.get(n);
|
||||
if (exact?.length) return exact;
|
||||
// "Ziggo Sport" without number → all Ziggo Sport*
|
||||
if (n === "ziggo sport") {
|
||||
const all: string[] = [];
|
||||
for (const [name, ids] of index.byChannelName) {
|
||||
|
|
@ -283,6 +427,50 @@ function channelIdsFor(index: CacheState, channel: string | null | undefined): s
|
|||
return [];
|
||||
}
|
||||
|
||||
function channelLogoFallback(
|
||||
index: CacheState,
|
||||
channel: string | null | undefined,
|
||||
network: string | null | undefined,
|
||||
existingImages: string[]
|
||||
): string | null {
|
||||
const fromImages = existingImages.find((u) => isChannelLogoUrl(u) && !isLiveChannelThumb(u));
|
||||
if (fromImages) return fromImages;
|
||||
for (const id of [...channelIdsFor(index, channel), ...channelIdsFor(index, network)]) {
|
||||
const logo = index.logoByChannelId.get(id);
|
||||
if (logo) return logo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Never keep a live-on-air thumb as card art (football under racing). */
|
||||
export function stripLiveThumbs<T extends EnrichableEvent>(
|
||||
e: T,
|
||||
fallbackLogo: string | null
|
||||
): T {
|
||||
const cleaned = e.images.filter((u) => !isLiveChannelThumb(u));
|
||||
const landscape =
|
||||
(e.imageLandscape && !isLiveChannelThumb(e.imageLandscape)
|
||||
? e.imageLandscape
|
||||
: null) ||
|
||||
cleaned.find((u) => !isChannelLogoUrl(u)) ||
|
||||
fallbackLogo ||
|
||||
cleaned[0] ||
|
||||
null;
|
||||
const backdrop =
|
||||
e.imageBackdrop && !isLiveChannelThumb(e.imageBackdrop)
|
||||
? e.imageBackdrop
|
||||
: landscape;
|
||||
const images =
|
||||
landscape && !cleaned.includes(landscape) ? [landscape, ...cleaned] : cleaned;
|
||||
return {
|
||||
...e,
|
||||
imageLandscape: landscape,
|
||||
imageBackdrop: backdrop,
|
||||
logoUrl: e.logoUrl || fallbackLogo,
|
||||
images,
|
||||
};
|
||||
}
|
||||
|
||||
export type EnrichableEvent = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -300,7 +488,7 @@ export type EnrichableEvent = {
|
|||
|
||||
/**
|
||||
* Replace live-thumbs / empty art on Ziggo Sport rows with Gracenote EPG art.
|
||||
* Soft-fails (returns input) if Ziggo GO is unreachable.
|
||||
* Soft-fails (returns input) if Ziggo GO is unreachable — but still strips live thumbs.
|
||||
*/
|
||||
export async function enrichZiggoGoArt<T extends EnrichableEvent>(events: T[]): Promise<T[]> {
|
||||
const targets = events.filter(needsZiggoArtEnrichment);
|
||||
|
|
@ -317,18 +505,24 @@ export async function enrichZiggoGoArt<T extends EnrichableEvent>(events: T[]):
|
|||
if (!Number.isFinite(minMs) || maxMs <= 0) return events;
|
||||
|
||||
const index = await loadZiggoIndex(minMs, maxMs);
|
||||
if (!index?.events.length) return events;
|
||||
|
||||
return events.map((e) => {
|
||||
if (!needsZiggoArtEnrichment(e)) return e;
|
||||
|
||||
// If Ziggo index unavailable: still remove live thumbs
|
||||
if (!index?.events.length) {
|
||||
return stripLiveThumbs(e, e.images.find(isChannelLogoUrl) ?? null);
|
||||
}
|
||||
|
||||
const ids = new Set([
|
||||
...channelIdsFor(index, e.channel),
|
||||
...channelIdsFor(index, e.network),
|
||||
]);
|
||||
if (!ids.size) return e;
|
||||
const startMs = Date.parse(e.start);
|
||||
const endMs = Date.parse(e.stop);
|
||||
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) return e;
|
||||
if (!ids.size || !Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
||||
return stripLiveThumbs(e, channelLogoFallback(index, e.channel, e.network, e.images));
|
||||
}
|
||||
|
||||
const candidates = index.events.filter((z) => ids.has(z.channelId));
|
||||
const match = pickBestZiggoMatch(candidates, {
|
||||
|
|
@ -336,9 +530,15 @@ export async function enrichZiggoGoArt<T extends EnrichableEvent>(events: T[]):
|
|||
startMs,
|
||||
endMs,
|
||||
});
|
||||
if (!match?.mergedId) return e;
|
||||
|
||||
if (!match?.mergedId) {
|
||||
return stripLiveThumbs(e, channelLogoFallback(index, e.channel, e.network, e.images));
|
||||
}
|
||||
|
||||
const arts = artUrlsFromMergedId(match.mergedId);
|
||||
if (!arts) return e;
|
||||
if (!arts) {
|
||||
return stripLiveThumbs(e, channelLogoFallback(index, e.channel, e.network, e.images));
|
||||
}
|
||||
|
||||
const images = [
|
||||
arts.landscape,
|
||||
|
|
@ -355,10 +555,12 @@ export async function enrichZiggoGoArt<T extends EnrichableEvent>(events: T[]):
|
|||
return {
|
||||
...e,
|
||||
imageLandscape: arts.landscape,
|
||||
imageBackdrop: e.imageBackdrop && !isLiveChannelThumb(e.imageBackdrop)
|
||||
imageBackdrop:
|
||||
e.imageBackdrop && !isLiveChannelThumb(e.imageBackdrop)
|
||||
? e.imageBackdrop
|
||||
: arts.landscape,
|
||||
imagePortrait: arts.portrait,
|
||||
logoUrl: e.logoUrl || channelLogoFallback(index, e.channel, e.network, e.images),
|
||||
images: deduped,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue