115 lines
3.9 KiB
TypeScript
115 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import type { ScheduleSlot } from "@/app/(main)/tv/components";
|
|
import type { ScheduledSlotResponse } from "@/lib/types";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pure transformation utilities
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Format an ISO-8601 string to "HH:MM" in the user's local timezone. */
|
|
export function fmtTime(iso: string): string {
|
|
return new Date(iso).toLocaleTimeString([], {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
});
|
|
}
|
|
|
|
/** Progress percentage through a slot based on wall-clock time. */
|
|
export function calcProgress(startAt: string, durationSecs: number): number {
|
|
if (durationSecs <= 0) return 0;
|
|
const elapsedSecs = (Date.now() - new Date(startAt).getTime()) / 1000;
|
|
return Math.min(100, Math.max(0, Math.round((elapsedSecs / durationSecs) * 100)));
|
|
}
|
|
|
|
/**
|
|
* Seconds elapsed since a slot started, computed from the current wall clock.
|
|
* Use this instead of `broadcast.offset_secs` when the broadcast may be cached —
|
|
* start_at is a fixed timestamp so the offset is always accurate regardless of
|
|
* when the broadcast response was fetched.
|
|
*/
|
|
export function calcOffsetSecs(startAt: string): number {
|
|
return Math.max(0, (Date.now() - new Date(startAt).getTime()) / 1000);
|
|
}
|
|
|
|
/** Minutes until a future timestamp (rounded, minimum 0). */
|
|
export function minutesUntil(iso: string): number {
|
|
return Math.max(0, Math.round((new Date(iso).getTime() - Date.now()) / 60_000));
|
|
}
|
|
|
|
/**
|
|
* Map EPG slots to the shape expected by ScheduleOverlay.
|
|
* Marks the slot matching currentSlotId as current.
|
|
*/
|
|
export function toScheduleSlots(
|
|
slots: ScheduledSlotResponse[],
|
|
currentSlotId?: string,
|
|
): ScheduleSlot[] {
|
|
return slots.map((slot) => {
|
|
const item = slot.item;
|
|
const isEpisode = item.content_type === "episode";
|
|
|
|
// Headline: series name for episodes (fall back to episode title), film title otherwise
|
|
const title = isEpisode && item.series_name ? item.series_name : item.title;
|
|
|
|
// Subtitle: episode identifier + title, or year for films
|
|
let subtitle: string | null = null;
|
|
if (isEpisode) {
|
|
const epParts: string[] = [];
|
|
if (item.season_number != null) epParts.push(`S${item.season_number}`);
|
|
if (item.episode_number != null) epParts.push(`E${item.episode_number}`);
|
|
const epLabel = epParts.join(" · ");
|
|
subtitle = item.series_name
|
|
? [epLabel, item.title].filter(Boolean).join(" · ")
|
|
: epLabel || null;
|
|
} else if (item.year) {
|
|
subtitle = String(item.year);
|
|
}
|
|
|
|
const durationMins = Math.round(
|
|
(new Date(slot.end_at).getTime() - new Date(slot.start_at).getTime()) / 60_000,
|
|
);
|
|
|
|
return {
|
|
id: slot.id,
|
|
title,
|
|
subtitle,
|
|
durationMins,
|
|
startTime: fmtTime(slot.start_at),
|
|
endTime: fmtTime(slot.end_at),
|
|
isCurrent: slot.id === currentSlotId,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find the slot immediately after the current one.
|
|
* Returns null if current slot is last or not found.
|
|
*/
|
|
export function findNextSlot(
|
|
slots: ScheduledSlotResponse[],
|
|
currentSlotId?: string,
|
|
): ScheduledSlotResponse | null {
|
|
if (!currentSlotId || slots.length === 0) return null;
|
|
const idx = slots.findIndex((s) => s.id === currentSlotId);
|
|
if (idx === -1 || idx === slots.length - 1) return null;
|
|
return slots[idx + 1];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// useStreamUrl — HLS playlist from the Playout Service
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PLAYOUT_URL =
|
|
process.env.NEXT_PUBLIC_PLAYOUT_URL ?? "http://localhost:9090";
|
|
|
|
export function useStreamUrl(channelId: string | undefined) {
|
|
if (!channelId) return { data: null, isLoading: false, error: null };
|
|
return {
|
|
data: `${PLAYOUT_URL}/playout/${channelId}/playlist.m3u8`,
|
|
isLoading: false,
|
|
error: null,
|
|
};
|
|
}
|