import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { motion } from 'framer-motion' import { Radio, CalendarEvent, FileVideo, Play, Clock } from '../lib/icons' import { useLiveTvInfo, useLiveTvChannels, useLiveTvRecordings, useLiveTvTimers } from '../hooks/use-jellyfin' import { jellyfinClient, getImageUrl } from '../api/jellyfin' type Tab = 'channels' | 'recordings' | 'scheduled' /** * Live TV / DVR landing page. Three tabs surfaced off Jellyfin's * LiveTv API: * - Channels (with current-program inline) * - Recordings (past + in-progress) * - Scheduled timers * * If Live TV isn't configured server-side, the page shows a friendly * placeholder instead of empty lists. */ export default function LiveTvPage() { const info = useLiveTvInfo() const [tab, setTab] = useState('channels') const enabled = info.data?.IsEnabled !== false const noTuners = !info.data?.Services?.length if (!info.isLoading && (!enabled || noTuners)) { return (

Live TV isn't set up on this server

Add a tuner and a guide provider in the Jellyfin dashboard under Live TV. Once configured, your channels and DVR recordings will show up here.

) } return (
setTab('channels')} icon={}> Channels setTab('recordings')} icon={}> Recordings setTab('scheduled')} icon={}> Scheduled
{tab === 'channels' && } {tab === 'recordings' && } {tab === 'scheduled' && }
) } function Header() { return (

Live TV

Channels & recordings

) } function TabButton({ active, onClick, children, icon, }: { active: boolean onClick: () => void children: React.ReactNode icon: React.ReactNode }) { return ( ) } function ChannelsList() { const navigate = useNavigate() const serverUrl = jellyfinClient.getAuthState()?.serverUrl || '' const { data: channels = [], isLoading } = useLiveTvChannels() if (isLoading) return if (channels.length === 0) { return No channels available right now. } return ( ) } function RecordingsList() { const navigate = useNavigate() const serverUrl = jellyfinClient.getAuthState()?.serverUrl || '' const { data: recordings = [], isLoading } = useLiveTvRecordings() if (isLoading) return if (recordings.length === 0) { return No recordings yet. Schedule one from a program to get started. } return (
    {recordings.map(r => { const imageTag = r.ImageTags?.Primary const img = r.Id && imageTag ? getImageUrl(serverUrl, r.Id, 'Primary', 220, imageTag) : '' return (
  • ) })}
) } function ScheduledList() { const { data: timers = [], isLoading } = useLiveTvTimers() if (isLoading) return if (timers.length === 0) { return Nothing on the schedule. } return (
    {timers.map((t: any) => (
  • {t.Name || t.ProgramName || 'Scheduled recording'}

    {t.StartDate ? new Date(t.StartDate).toLocaleString(undefined, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : ''} {t.ChannelName && - {t.ChannelName}}

    {t.Status && ( {t.Status} )}
  • ))}
) } function programTime(start?: string | null, end?: string | null): string { if (!start) return '' const s = new Date(start) const e = end ? new Date(end) : null if (Number.isNaN(s.getTime())) return '' const fmt = (d: Date) => d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) return e ? `${fmt(s)}-${fmt(e)} ` : `${fmt(s)} ` } function Skeleton() { return (
{Array.from({ length: 6 }).map((_, i) => (
))}
) } function EmptyMsg({ children }: { children: React.ReactNode }) { return (

{children}

) }