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 today = payload.today.sportFiltered()
|
||||||
val weekend = payload.weekend.sportFiltered()
|
val weekend = payload.weekend.sportFiltered()
|
||||||
val later = payload.later.sportFiltered()
|
val later = payload.later.sportFiltered()
|
||||||
val catchup = payload.catchup.sportFiltered()
|
|
||||||
val sports = payload.sports
|
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() ||
|
val hasSchedule = live.isNotEmpty() || today.isNotEmpty() ||
|
||||||
weekend.isNotEmpty() || later.isNotEmpty() || catchup.isNotEmpty()
|
weekend.isNotEmpty() || later.isNotEmpty()
|
||||||
val hasAny = hasSchedule || rails.isNotEmpty()
|
val hasAny = hasSchedule || rails.isNotEmpty()
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -207,7 +209,7 @@ fun ViaplayHubScreen(
|
||||||
fontWeight = FontWeight.Black,
|
fontWeight = FontWeight.Black,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Live · agenda · samenvattingen · docs",
|
"Live · agenda · Formule 1",
|
||||||
color = VpLook.Mist,
|
color = VpLook.Mist,
|
||||||
fontSize = 14.sp,
|
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 ->
|
rails.forEach { rail ->
|
||||||
item(key = "rail-${rail.id}") {
|
item(key = "rail-${rail.id}") {
|
||||||
VpCatalogRail(
|
VpCatalogRail(
|
||||||
title = rail.title,
|
title = rail.title,
|
||||||
|
subtitle = if (rail.id == "f1-replay") "Races, kwalificatie en hoogtepunten" else null,
|
||||||
accent = VpLook.Accent,
|
accent = VpLook.Accent,
|
||||||
items = rail.items,
|
items = rail.items,
|
||||||
onOpen = ::onItem,
|
onOpen = ::onItem,
|
||||||
|
|
@ -367,19 +358,29 @@ private fun VpEventRail(
|
||||||
@Composable
|
@Composable
|
||||||
private fun VpCatalogRail(
|
private fun VpCatalogRail(
|
||||||
title: String,
|
title: String,
|
||||||
|
subtitle: String? = null,
|
||||||
accent: Color,
|
accent: Color,
|
||||||
items: List<ViaplayHomeItem>,
|
items: List<ViaplayHomeItem>,
|
||||||
onOpen: (ViaplayHomeItem) -> Unit,
|
onOpen: (ViaplayHomeItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(top = 18.dp, bottom = 6.dp)) {
|
Column(Modifier.padding(top = 18.dp, bottom = 6.dp)) {
|
||||||
|
Column(Modifier.padding(horizontal = HomeChrome.EdgeInset, vertical = 6.dp)) {
|
||||||
Text(
|
Text(
|
||||||
title.uppercase(Locale("nl", "NL")),
|
title.uppercase(Locale("nl", "NL")),
|
||||||
color = accent,
|
color = accent,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
letterSpacing = 1.2.sp,
|
letterSpacing = 1.2.sp,
|
||||||
modifier = Modifier.padding(horizontal = HomeChrome.EdgeInset, vertical = 6.dp),
|
|
||||||
)
|
)
|
||||||
|
if (!subtitle.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
color = VpLook.Mist,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset),
|
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
|
|
||||||
|
|
@ -568,9 +568,14 @@ struct ViaplayHubScreen: View {
|
||||||
let today = filtered(p.today)
|
let today = filtered(p.today)
|
||||||
let weekend = filtered(p.weekend)
|
let weekend = filtered(p.weekend)
|
||||||
let later = filtered(p.later)
|
let later = filtered(p.later)
|
||||||
let catchup = filtered(p.catchup)
|
let rails = p.rails.filter { rail in
|
||||||
let rails = p.rails.filter { !$0.items.isEmpty }
|
guard !rail.items.isEmpty else { return false }
|
||||||
let noEvents = live.isEmpty && today.isEmpty && weekend.isEmpty && later.isEmpty && catchup.isEmpty
|
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) {
|
VStack(alignment: .leading, spacing: 28) {
|
||||||
hero(featured: live.first ?? today.first ?? weekend.first ?? later.first, layout)
|
hero(featured: live.first ?? today.first ?? weekend.first ?? later.first, layout)
|
||||||
|
|
@ -595,10 +600,13 @@ struct ViaplayHubScreen: View {
|
||||||
eventRail("Vandaag", today, width: 240)
|
eventRail("Vandaag", today, width: 240)
|
||||||
eventRail("Dit weekend", weekend, width: 240)
|
eventRail("Dit weekend", weekend, width: 240)
|
||||||
eventRail("Binnenkort", later, width: 240)
|
eventRail("Binnenkort", later, width: 240)
|
||||||
eventRail("Terugkijken", catchup, width: 240)
|
|
||||||
|
|
||||||
ForEach(rails) { rail in
|
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
|
HubRail(items: rail.items) { item in
|
||||||
itemTap(item) {
|
itemTap(item) {
|
||||||
HubContentCard(item: item, theme: theme, width: 240)
|
HubContentCard(item: item, theme: theme, width: 240)
|
||||||
|
|
@ -642,7 +650,7 @@ struct ViaplayHubScreen: View {
|
||||||
eyebrow: "Viaplay",
|
eyebrow: "Viaplay",
|
||||||
isLive: false,
|
isLive: false,
|
||||||
title: "Viaplay Sport",
|
title: "Viaplay Sport",
|
||||||
subtitle: "Live sport, samenvattingen en meer"
|
subtitle: "Live, agenda en Formule 1 terugkijken"
|
||||||
) { EmptyView() }
|
) { EmptyView() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,6 @@ import {
|
||||||
import type { ViaplayEventSnapshot } from "./tweaks";
|
import type { ViaplayEventSnapshot } from "./tweaks";
|
||||||
import { toMediaGuid } from "./accounts";
|
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" {
|
function phaseFor(startMs: number, endMs: number, nowMs: number): "live" | "upcoming" | "ended" {
|
||||||
if (nowMs < startMs) return "upcoming";
|
if (nowMs < startMs) return "upcoming";
|
||||||
if (nowMs > endMs) return "ended";
|
if (nowMs > endMs) return "ended";
|
||||||
|
|
@ -33,7 +30,6 @@ 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 catchupOk = phase === "ended" && nowMs <= event.endMs + VIAPLAY_CATCHUP_MS;
|
|
||||||
return {
|
return {
|
||||||
id: toEventId(event.mediaGuid),
|
id: toEventId(event.mediaGuid),
|
||||||
name: event.title,
|
name: event.title,
|
||||||
|
|
@ -46,7 +42,7 @@ export function eventToPublic(
|
||||||
network: "Viaplay",
|
network: "Viaplay",
|
||||||
isLive: phase === "live",
|
isLive: phase === "live",
|
||||||
phase,
|
phase,
|
||||||
playable: phase === "live" || catchupOk,
|
playable: phase === "live",
|
||||||
status: phase,
|
status: phase,
|
||||||
start: new Date(event.startMs).toISOString(),
|
start: new Date(event.startMs).toISOString(),
|
||||||
stop: new Date(event.endMs).toISOString(),
|
stop: new Date(event.endMs).toISOString(),
|
||||||
|
|
@ -72,7 +68,7 @@ export async function listViaplayScheduleEvents(
|
||||||
const horizon = nowMs + aheadMs;
|
const horizon = nowMs + aheadMs;
|
||||||
const out: ScheduleEventPublic[] = [];
|
const out: ScheduleEventPublic[] = [];
|
||||||
for (const event of config.events) {
|
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.endMs < nowMs) continue;
|
||||||
if (event.startMs > horizon) continue;
|
if (event.startMs > horizon) continue;
|
||||||
const pub = eventToPublic(event, nowMs);
|
const pub = eventToPublic(event, nowMs);
|
||||||
|
|
@ -139,7 +135,7 @@ export type ViaplayHomePayload = {
|
||||||
weekend: ScheduleEventPublic[];
|
weekend: ScheduleEventPublic[];
|
||||||
/** Upcoming daarna binnen sync-horizon */
|
/** Upcoming daarna binnen sync-horizon */
|
||||||
later: ScheduleEventPublic[];
|
later: ScheduleEventPublic[];
|
||||||
/** Recent afgelopen binnen catch-up venster */
|
/** Leeg: generic EPG-catch-up is uit. F1-terugkijken zit in `rails`. */
|
||||||
catchup: ScheduleEventPublic[];
|
catchup: ScheduleEventPublic[];
|
||||||
/** Sport-chips aanwezig in de payload */
|
/** Sport-chips aanwezig in de payload */
|
||||||
sports: ViaplaySportChip[];
|
sports: ViaplaySportChip[];
|
||||||
|
|
@ -184,10 +180,7 @@ function isWeekendLocal(ms: number): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function railItemToHome(item: ViaplaySportRailItem, nowMs: number): ViaplayHomeItem {
|
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";
|
||||||
const isEvent =
|
|
||||||
(item.kind === "sport" && item.playMode === "live") ||
|
|
||||||
(item.kind === "sport" && epg);
|
|
||||||
const openAs: ViaplayHomeItem["openAs"] = isEvent ? "event" : "content";
|
const openAs: ViaplayHomeItem["openAs"] = isEvent ? "event" : "content";
|
||||||
const id =
|
const id =
|
||||||
openAs === "event" ? toEventId(toMediaGuid(item.guid)) : `viaplay-content:${item.guid}`;
|
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;
|
let sportRailsCache: { at: number; rails: ViaplayHomeRail[] } | null = null;
|
||||||
const SPORT_RAILS_TTL_MS = 5 * 60_000;
|
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> {
|
export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePayload> {
|
||||||
const config = await loadViaplayConfigOnly();
|
const config = await loadViaplayConfigOnly();
|
||||||
const empty: ViaplayHomePayload = {
|
const empty: ViaplayHomePayload = {
|
||||||
|
|
@ -267,20 +300,15 @@ export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePay
|
||||||
const todayYmd = localYmd(nowMs);
|
const todayYmd = localYmd(nowMs);
|
||||||
|
|
||||||
const all: ScheduleEventPublic[] = [];
|
const all: ScheduleEventPublic[] = [];
|
||||||
const catchup: ScheduleEventPublic[] = [];
|
|
||||||
for (const event of config.events || []) {
|
for (const event of config.events || []) {
|
||||||
if (event.startMs > horizon) continue;
|
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;
|
if (event.endMs < nowMs) continue;
|
||||||
|
const pub = eventToPublic(event, nowMs);
|
||||||
|
if (pub.phase === "ended") continue;
|
||||||
all.push(pub);
|
all.push(pub);
|
||||||
}
|
}
|
||||||
|
|
||||||
all.sort((a, b) => Date.parse(a.start) - Date.parse(b.start));
|
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 live = all.filter((e) => e.phase === "live");
|
||||||
const upcoming = all.filter((e) => e.phase === "upcoming");
|
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 }>();
|
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 id = e.sportBucket || "other";
|
||||||
const label = e.sportLabel || id;
|
const label = e.sportLabel || id;
|
||||||
const cur = sportCounts.get(id) ?? { label, count: 0 };
|
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 }))
|
.map(([id, v]) => ({ id, label: v.label, count: v.count }))
|
||||||
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label, "nl"));
|
.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 {
|
return {
|
||||||
generatedAt: new Date(nowMs).toISOString(),
|
generatedAt: new Date(nowMs).toISOString(),
|
||||||
|
|
@ -327,7 +355,7 @@ export async function getViaplayHome(nowMs = Date.now()): Promise<ViaplayHomePay
|
||||||
today,
|
today,
|
||||||
weekend,
|
weekend,
|
||||||
later,
|
later,
|
||||||
catchup: catchup.slice(0, 40),
|
catchup: [],
|
||||||
sports,
|
sports,
|
||||||
events: all,
|
events: all,
|
||||||
rails,
|
rails,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ import {
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { findViaplayEvent } from "./sync";
|
import { findViaplayEvent } from "./sync";
|
||||||
import { isMpdCacheFresh, type ViaplayEventSnapshot } from "./tweaks";
|
import { isMpdCacheFresh, type ViaplayEventSnapshot } from "./tweaks";
|
||||||
import { VIAPLAY_CATCHUP_MS } from "./events";
|
|
||||||
|
|
||||||
export type LastVodPlayDiag = ViaplayPlayDiag;
|
export type LastVodPlayDiag = ViaplayPlayDiag;
|
||||||
|
|
||||||
|
|
@ -968,8 +967,7 @@ export async function resolveViaplayPlay(
|
||||||
if (!event) throw new AppError("NOT_FOUND", "Viaplay-event niet gevonden", 404);
|
if (!event) throw new AppError("NOT_FOUND", "Viaplay-event niet gevonden", 404);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// Catch-up: officieel ~48u; live-pad (play-live + KAP) blijft na einde bruikbaar.
|
if (now > event.endMs) {
|
||||||
if (now > event.endMs + VIAPLAY_CATCHUP_MS) {
|
|
||||||
throw new AppError("GONE", "Dit event is afgelopen", 410);
|
throw new AppError("GONE", "Dit event is afgelopen", 410);
|
||||||
}
|
}
|
||||||
return await ensureViaplayEventReady(sessionSecret, event);
|
return await ensureViaplayEventReady(sessionSecret, event);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue