Limit Viaplay hub catch-up to F1 clips; skip ended EPG play.
This commit is contained in:
parent
7cfe2598a8
commit
84657acbc9
4 changed files with 90 additions and 55 deletions
|
|
@ -178,11 +178,13 @@ fun ViaplayHubScreen(
|
|||
val today = payload.today.sportFiltered()
|
||||
val weekend = payload.weekend.sportFiltered()
|
||||
val later = payload.later.sportFiltered()
|
||||
val catchup = payload.catchup.sportFiltered()
|
||||
val sports = payload.sports
|
||||
val rails = payload.rails
|
||||
val rails = payload.rails.filter { it.items.isNotEmpty() }.filter { rail ->
|
||||
val f = sportFilter
|
||||
f == null || rail.id != "f1-replay" || f.equals("racing", ignoreCase = true)
|
||||
}
|
||||
val hasSchedule = live.isNotEmpty() || today.isNotEmpty() ||
|
||||
weekend.isNotEmpty() || later.isNotEmpty() || catchup.isNotEmpty()
|
||||
weekend.isNotEmpty() || later.isNotEmpty()
|
||||
val hasAny = hasSchedule || rails.isNotEmpty()
|
||||
|
||||
LazyColumn(
|
||||
|
|
@ -207,7 +209,7 @@ fun ViaplayHubScreen(
|
|||
fontWeight = FontWeight.Black,
|
||||
)
|
||||
Text(
|
||||
"Live · agenda · samenvattingen · docs",
|
||||
"Live · agenda · Formule 1",
|
||||
color = VpLook.Mist,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
|
|
@ -305,23 +307,12 @@ fun ViaplayHubScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
if (catchup.isNotEmpty()) {
|
||||
item(key = "catchup") {
|
||||
VpEventRail(
|
||||
title = "Terugkijken",
|
||||
accent = VpLook.Accent,
|
||||
events = catchup,
|
||||
nowMs = nowMs,
|
||||
onOpen = onOpenEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Officiële sport-sectie planken (HAR: /sport)
|
||||
rails.forEach { rail ->
|
||||
item(key = "rail-${rail.id}") {
|
||||
VpCatalogRail(
|
||||
title = rail.title,
|
||||
subtitle = if (rail.id == "f1-replay") "Races, kwalificatie en hoogtepunten" else null,
|
||||
accent = VpLook.Accent,
|
||||
items = rail.items,
|
||||
onOpen = ::onItem,
|
||||
|
|
@ -367,19 +358,29 @@ private fun VpEventRail(
|
|||
@Composable
|
||||
private fun VpCatalogRail(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
accent: Color,
|
||||
items: List<ViaplayHomeItem>,
|
||||
onOpen: (ViaplayHomeItem) -> Unit,
|
||||
) {
|
||||
Column(Modifier.padding(top = 18.dp, bottom = 6.dp)) {
|
||||
Text(
|
||||
title.uppercase(Locale("nl", "NL")),
|
||||
color = accent,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.2.sp,
|
||||
modifier = Modifier.padding(horizontal = HomeChrome.EdgeInset, vertical = 6.dp),
|
||||
)
|
||||
Column(Modifier.padding(horizontal = HomeChrome.EdgeInset, vertical = 6.dp)) {
|
||||
Text(
|
||||
title.uppercase(Locale("nl", "NL")),
|
||||
color = accent,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.2.sp,
|
||||
)
|
||||
if (!subtitle.isNullOrBlank()) {
|
||||
Text(
|
||||
subtitle,
|
||||
color = VpLook.Mist,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
|
|
|
|||
|
|
@ -568,9 +568,14 @@ struct ViaplayHubScreen: View {
|
|||
let today = filtered(p.today)
|
||||
let weekend = filtered(p.weekend)
|
||||
let later = filtered(p.later)
|
||||
let catchup = filtered(p.catchup)
|
||||
let rails = p.rails.filter { !$0.items.isEmpty }
|
||||
let noEvents = live.isEmpty && today.isEmpty && weekend.isEmpty && later.isEmpty && catchup.isEmpty
|
||||
let rails = p.rails.filter { rail in
|
||||
guard !rail.items.isEmpty else { return false }
|
||||
if rail.id == "f1-replay", let sport {
|
||||
return sport.caseInsensitiveCompare("racing") == .orderedSame
|
||||
}
|
||||
return true
|
||||
}
|
||||
let noEvents = live.isEmpty && today.isEmpty && weekend.isEmpty && later.isEmpty && rails.isEmpty
|
||||
|
||||
VStack(alignment: .leading, spacing: 28) {
|
||||
hero(featured: live.first ?? today.first ?? weekend.first ?? later.first, layout)
|
||||
|
|
@ -595,10 +600,13 @@ struct ViaplayHubScreen: View {
|
|||
eventRail("Vandaag", today, width: 240)
|
||||
eventRail("Dit weekend", weekend, width: 240)
|
||||
eventRail("Binnenkort", later, width: 240)
|
||||
eventRail("Terugkijken", catchup, width: 240)
|
||||
|
||||
ForEach(rails) { rail in
|
||||
HubSection(title: rail.title) {
|
||||
HubSection(
|
||||
title: rail.title,
|
||||
subtitle: rail.id == "f1-replay" ? "Races, kwalificatie en hoogtepunten" : nil,
|
||||
count: rail.items.count
|
||||
) {
|
||||
HubRail(items: rail.items) { item in
|
||||
itemTap(item) {
|
||||
HubContentCard(item: item, theme: theme, width: 240)
|
||||
|
|
@ -642,7 +650,7 @@ struct ViaplayHubScreen: View {
|
|||
eyebrow: "Viaplay",
|
||||
isLive: false,
|
||||
title: "Viaplay Sport",
|
||||
subtitle: "Live sport, samenvattingen en meer"
|
||||
subtitle: "Live, agenda en Formule 1 terugkijken"
|
||||
) { EmptyView() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ import {
|
|||
import type { ViaplayEventSnapshot } from "./tweaks";
|
||||
import { toMediaGuid } from "./accounts";
|
||||
|
||||
/** Officieel Viaplay-terugkijken venster (~48u na start/einde). */
|
||||
export const VIAPLAY_CATCHUP_MS = 48 * 60 * 60_000;
|
||||
|
||||
function phaseFor(startMs: number, endMs: number, nowMs: number): "live" | "upcoming" | "ended" {
|
||||
if (nowMs < startMs) return "upcoming";
|
||||
if (nowMs > endMs) return "ended";
|
||||
|
|
@ -33,7 +30,6 @@ export function eventToPublic(
|
|||
nowMs: number
|
||||
): ScheduleEventPublic {
|
||||
const phase = phaseFor(event.startMs, event.endMs, nowMs);
|
||||
const catchupOk = phase === "ended" && nowMs <= event.endMs + VIAPLAY_CATCHUP_MS;
|
||||
return {
|
||||
id: toEventId(event.mediaGuid),
|
||||
name: event.title,
|
||||
|
|
@ -46,7 +42,7 @@ export function eventToPublic(
|
|||
network: "Viaplay",
|
||||
isLive: phase === "live",
|
||||
phase,
|
||||
playable: phase === "live" || catchupOk,
|
||||
playable: phase === "live",
|
||||
status: phase,
|
||||
start: new Date(event.startMs).toISOString(),
|
||||
stop: new Date(event.endMs).toISOString(),
|
||||
|
|
@ -72,7 +68,7 @@ export async function listViaplayScheduleEvents(
|
|||
const horizon = nowMs + aheadMs;
|
||||
const out: ScheduleEventPublic[] = [];
|
||||
for (const event of config.events) {
|
||||
// Hub/Events: live + upcoming; catch-up zit in Viaplay-hub rails / detail play.
|
||||
// Hub/Events: live + upcoming. Terugkijken alleen via F1-clips in de hub.
|
||||
if (event.endMs < nowMs) continue;
|
||||
if (event.startMs > horizon) continue;
|
||||
const pub = eventToPublic(event, nowMs);
|
||||
|
|
@ -139,7 +135,7 @@ export type ViaplayHomePayload = {
|
|||
weekend: ScheduleEventPublic[];
|
||||
/** Upcoming daarna binnen sync-horizon */
|
||||
later: ScheduleEventPublic[];
|
||||
/** Recent afgelopen binnen catch-up venster */
|
||||
/** Leeg: generic EPG-catch-up is uit. F1-terugkijken zit in `rails`. */
|
||||
catchup: ScheduleEventPublic[];
|
||||
/** Sport-chips aanwezig in de payload */
|
||||
sports: ViaplaySportChip[];
|
||||
|
|
@ -184,10 +180,7 @@ function isWeekendLocal(ms: number): boolean {
|
|||
}
|
||||
|
||||
function railItemToHome(item: ViaplaySportRailItem, nowMs: number): ViaplayHomeItem {
|
||||
const epg = /^S\d+/i.test(String(item.guid || "").replace(/-NL$/i, ""));
|
||||
const isEvent =
|
||||
(item.kind === "sport" && item.playMode === "live") ||
|
||||
(item.kind === "sport" && epg);
|
||||
const isEvent = item.kind === "sport" && item.playMode === "live";
|
||||
const openAs: ViaplayHomeItem["openAs"] = isEvent ? "event" : "content";
|
||||
const id =
|
||||
openAs === "event" ? toEventId(toMediaGuid(item.guid)) : `viaplay-content:${item.guid}`;
|
||||
|
|
@ -227,6 +220,46 @@ function mapSportRails(rails: ViaplaySportRail[], nowMs: number): ViaplayHomeRai
|
|||
}));
|
||||
}
|
||||
|
||||
const F1_RE =
|
||||
/formule[\s-]*1|formula[\s-]*1|\bformula1\b|\bf1\b|\/formule-1|\/formula-1|series-formula1/i;
|
||||
const EPG_GUID_RE = /^S\d{8,}$/i;
|
||||
|
||||
function isF1ReplayItem(item: ViaplayHomeItem, railTitle: string): boolean {
|
||||
if (item.openAs === "event") return false;
|
||||
if (item.kind === "series") return false;
|
||||
if (EPG_GUID_RE.test(String(item.guid || "").replace(/-NL$/i, ""))) return false;
|
||||
return F1_RE.test(`${railTitle} ${item.title} ${item.subtitle || ""} ${item.pageUrl || ""}`);
|
||||
}
|
||||
|
||||
function f1ReplayBadge(item: ViaplayHomeItem): string | null {
|
||||
const t = item.title.toLowerCase();
|
||||
if (t.includes("kwalificatie")) return "Kwalificatie";
|
||||
if (t.includes("sprintrace") || t.includes("sprintkwali") || /\bsprint\b/.test(t)) {
|
||||
return "Sprint";
|
||||
}
|
||||
if (/^race\b/.test(t) || t.includes("race |") || t.includes("race –") || t.includes("race -")) {
|
||||
return "Race";
|
||||
}
|
||||
if (t.includes("terugblik") || t.includes("highlight") || t.includes("hoogtepunten")) {
|
||||
return "Hoogtepunten";
|
||||
}
|
||||
return item.kind === "clip" ? "F1" : item.badge;
|
||||
}
|
||||
|
||||
function f1ReplayRails(raw: ViaplayHomeRail[]): ViaplayHomeRail[] {
|
||||
const seen = new Set<string>();
|
||||
const items: ViaplayHomeItem[] = [];
|
||||
for (const rail of raw) {
|
||||
for (const item of rail.items) {
|
||||
if (!isF1ReplayItem(item, rail.title) || seen.has(item.guid)) continue;
|
||||
seen.add(item.guid);
|
||||
items.push({ ...item, badge: f1ReplayBadge(item) });
|
||||
}
|
||||
}
|
||||
if (!items.length) return [];
|
||||
return [{ id: "f1-replay", title: "Formule 1", items: items.slice(0, 48) }];
|
||||
}
|
||||
|
||||
let sportRailsCache: { at: number; rails: ViaplayHomeRail[] } | null = null;
|
||||
const SPORT_RAILS_TTL_MS = 5 * 60_000;
|
||||
|
||||
|
|
@ -246,7 +279,7 @@ async function loadSportRailsHome(nowMs: number): Promise<ViaplayHomeRail[]> {
|
|||
}
|
||||
}
|
||||
|
||||
/** Hub: tijd-rails + officiële sport-planken (samenvattingen/docs). */
|
||||
/** Hub: live + agenda, plus één Formule 1-terugkijkplank. */
|
||||
export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePayload> {
|
||||
const config = await loadViaplayConfigOnly();
|
||||
const empty: ViaplayHomePayload = {
|
||||
|
|
@ -267,20 +300,15 @@ export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePay
|
|||
const todayYmd = localYmd(nowMs);
|
||||
|
||||
const all: ScheduleEventPublic[] = [];
|
||||
const catchup: ScheduleEventPublic[] = [];
|
||||
for (const event of config.events || []) {
|
||||
if (event.startMs > horizon) continue;
|
||||
const pub = eventToPublic(event, nowMs);
|
||||
if (pub.phase === "ended") {
|
||||
if (pub.playable) catchup.push(pub);
|
||||
continue;
|
||||
}
|
||||
if (event.endMs < nowMs) continue;
|
||||
const pub = eventToPublic(event, nowMs);
|
||||
if (pub.phase === "ended") continue;
|
||||
all.push(pub);
|
||||
}
|
||||
|
||||
all.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
||||
catchup.sort((a, b) => Date.parse(b.start) - Date.parse(a.start));
|
||||
|
||||
const live = all.filter((e) => e.phase === "live");
|
||||
const upcoming = all.filter((e) => e.phase === "upcoming");
|
||||
|
|
@ -307,7 +335,7 @@ export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePay
|
|||
}
|
||||
|
||||
const sportCounts = new Map<string, { label: string; count: number }>();
|
||||
for (const e of [...all, ...catchup.slice(0, 40)]) {
|
||||
for (const e of all) {
|
||||
const id = e.sportBucket || "other";
|
||||
const label = e.sportLabel || id;
|
||||
const cur = sportCounts.get(id) ?? { label, count: 0 };
|
||||
|
|
@ -319,7 +347,7 @@ export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePay
|
|||
.map(([id, v]) => ({ id, label: v.label, count: v.count }))
|
||||
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label, "nl"));
|
||||
|
||||
const rails = await loadSportRailsHome(nowMs);
|
||||
const rails = f1ReplayRails(await loadSportRailsHome(nowMs));
|
||||
|
||||
return {
|
||||
generatedAt: new Date(nowMs).toISOString(),
|
||||
|
|
@ -327,7 +355,7 @@ export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePay
|
|||
today,
|
||||
weekend,
|
||||
later,
|
||||
catchup: catchup.slice(0, 40),
|
||||
catchup: [],
|
||||
sports,
|
||||
events: all,
|
||||
rails,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ import {
|
|||
} from "./settings";
|
||||
import { findViaplayEvent } from "./sync";
|
||||
import { isMpdCacheFresh, type ViaplayEventSnapshot } from "./tweaks";
|
||||
import { VIAPLAY_CATCHUP_MS } from "./events";
|
||||
|
||||
export type LastVodPlayDiag = ViaplayPlayDiag;
|
||||
|
||||
|
|
@ -968,8 +967,7 @@ export async function resolveViaplayPlay(
|
|||
if (!event) throw new AppError("NOT_FOUND", "Viaplay-event niet gevonden", 404);
|
||||
|
||||
const now = Date.now();
|
||||
// Catch-up: officieel ~48u; live-pad (play-live + KAP) blijft na einde bruikbaar.
|
||||
if (now > event.endMs + VIAPLAY_CATCHUP_MS) {
|
||||
if (now > event.endMs) {
|
||||
throw new AppError("GONE", "Dit event is afgelopen", 410);
|
||||
}
|
||||
return await ensureViaplayEventReady(sessionSecret, event);
|
||||
|
|
|
|||
Loading…
Reference in a new issue