"use client"; import { usePoll } from "@/lib/use-poll"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { StatusBadge } from "@/components/status-badge"; import { Badge } from "@/components/ui/badge"; import type { JellyfinLatestItem, ArrHistoryItem } from "@/lib/types"; import { CardSkeleton } from "@/components/skeleton"; import { EmptyState } from "@/components/empty-state"; import { TdarrCard } from "@/components/tdarr-card"; interface QueueItem { title: string; status: string; size?: string; timeleft?: string; progress?: number; } interface SonarrQueueItem { title: string; status: string; sizeleft?: string; timeleft?: string; } interface RadarrQueueItem { title: string; status: string; sizeleft?: string; timeleft?: string; } interface SabQueue { slots: QueueItem[]; speed: string; paused: boolean; } interface ProwlarrStats { total: number; enabled: number; indexers: { name: string; protocol: string }[]; error?: string; } interface BazarrStatus { version?: string; sonarr_signalr?: string; radarr_signalr?: string; wanted_episodes?: number; wanted_movies?: number; error?: string; } interface ABSLibrary { name: string; type: string; items: number; } interface ABSStats { libraries: ABSLibrary[]; total: number; error?: string; } interface DelugeStatus { available: boolean; total?: number; active?: number; downloading?: number; seeding?: number; error?: string; } interface JellyfinStatus { version: string; server_name: string; libraries: { name: string; type: string }[]; active_sessions: { user: string; client: string; device: string; now_playing: string; type: string; }[]; idle_sessions: number; } interface PlexSession { title: string; type: string; year?: string; player?: string; platform?: string; device?: string; state?: string; local?: boolean; user?: string; bandwidth?: string; location?: string; video_resolution?: string; video_codec?: string; media_bitrate?: string; transcode?: boolean; video_decision?: string; transcode_speed?: string; } interface PlexServer { name: string; url: string; online: boolean; sessions: PlexSession[]; libraries: { title: string; type: string }[]; error?: string; } interface PlexStatus { servers: PlexServer[]; } const serverColors: Record = { calypso: "text-violet-400", atlantis: "text-blue-400", jellyfin: "text-cyan-400", }; function colorizeServerName(name: string): string { const lower = name.toLowerCase(); for (const [key, cls] of Object.entries(serverColors)) { if (lower.includes(key)) return cls; } return "text-foreground"; } const libraryTypeColors: Record = { movies: "text-blue-400", movie: "text-blue-400", tvshows: "text-violet-400", tvshow: "text-violet-400", series: "text-violet-400", music: "text-green-400", anime: "text-pink-400", }; function getLibraryTypeColor(type: string): string { return libraryTypeColors[type.toLowerCase()] ?? "text-foreground"; } const serviceNameColors: Record = { sonarr: "text-blue-400", radarr: "text-amber-400", prowlarr: "text-violet-400", bazarr: "text-green-400", }; function formatShortDate(dateStr: string): string { try { const d = new Date(dateStr); return d.toLocaleDateString("en-US", { month: "short", day: "2-digit" }); } catch { return dateStr; } } const eventColors: Record = { grabbed: "text-blue-400", imported: "text-green-400", downloadfolderimported: "text-green-400", downloadfailed: "text-red-400", deleted: "text-red-400", renamed: "text-amber-400", upgraded: "text-violet-400", }; function getEventColor(event: string): string { return eventColors[event.toLowerCase()] ?? "text-muted-foreground"; } const mediaTypeColors: Record = { movie: "text-blue-400", episode: "text-violet-400", series: "text-violet-400", season: "text-violet-400", audio: "text-green-400", musicalbum: "text-green-400", }; export default function MediaPage() { const { data: jellyfin } = usePoll("/api/jellyfin/status", 30000); const { data: plex } = usePoll("/api/plex/status", 30000); const { data: sonarrRaw } = usePoll>("/api/sonarr/queue", 30000); const { data: radarrRaw } = usePoll>("/api/radarr/queue", 30000); const { data: sabRaw } = usePoll>("/api/sabnzbd/queue", 30000); const { data: prowlarr } = usePoll("/api/prowlarr/stats", 60000); const { data: bazarr } = usePoll("/api/bazarr/status", 60000); const { data: abs } = usePoll("/api/audiobookshelf/stats", 60000); const { data: deluge } = usePoll("/api/deluge/status", 30000); const { data: jellyfinLatest } = usePoll("/api/jellyfin/latest", 60000); const { data: sonarrHistory } = usePoll("/api/sonarr/history", 60000); const { data: radarrHistory } = usePoll("/api/radarr/history", 60000); const sonarr = (sonarrRaw?.records ?? sonarrRaw?.items ?? []) as SonarrQueueItem[]; const radarr = (radarrRaw?.records ?? radarrRaw?.items ?? []) as RadarrQueueItem[]; const sab = sabRaw?.queue as SabQueue | undefined; // Collect all active streams from both Jellyfin and Plex const allStreams: { title: string; player: string; device: string; source: string; transcode?: boolean; bandwidth?: string; state?: string; user?: string; quality?: string; location?: string; }[] = []; if (jellyfin?.active_sessions) { for (const s of jellyfin.active_sessions) { allStreams.push({ title: s.now_playing, player: s.client, device: s.device, source: `Jellyfin (${jellyfin.server_name})`, }); } } if (plex?.servers) { for (const server of plex.servers) { for (const s of server.sessions) { const quality = [ s.video_resolution?.toUpperCase(), s.video_codec?.toUpperCase(), ].filter(Boolean).join(" "); allStreams.push({ title: s.title, player: s.player ?? "?", device: s.device ?? s.platform ?? "?", source: `Plex (${server.name})`, transcode: s.transcode, bandwidth: s.bandwidth, state: s.state, user: s.user, quality, location: s.location, }); } } } return (

Media

{/* Tdarr Cluster */} {/* Now Playing Hero */} Now Playing {allStreams.length > 0 && ( {allStreams.length} stream{allStreams.length !== 1 ? "s" : ""} )} {allStreams.length === 0 ? ( "} title="Nothing playing" description="Start something on Jellyfin or Plex" /> ) : (
{allStreams.map((stream, i) => (

{stream.title}

{stream.user && ( {stream.user} )}
{stream.source} {stream.state && ( )} {stream.quality && ( {stream.quality} )} {stream.transcode && ( Transcoding )} {stream.location && ( {stream.location.toUpperCase()} )}
{stream.player} · {stream.device} {stream.bandwidth && ( <> · {(Number(stream.bandwidth) / 1000).toFixed(1)} Mbps )}
))}
)}
{/* Recently Added + Download History */}
{/* Recently Added to Jellyfin */} Recently Added {!jellyfinLatest ? ( ) : jellyfinLatest.length === 0 ? (

No recent additions

) : (
{jellyfinLatest.slice(0, 8).map((item, i) => (

{item.series ? `${item.series}: ${item.name}` : item.name} {item.year ? ` (${item.year})` : ""}

{item.type} {formatShortDate(item.date)}
))}
)}
{/* Sonarr History */} Sonarr History {!sonarrHistory ? ( ) : sonarrHistory.length === 0 ? (

No recent activity

) : (
{sonarrHistory.slice(0, 8).map((item, i) => (

{item.title}

{item.event} {item.quality} {formatShortDate(item.date)}
))}
)}
{/* Radarr History */} Radarr History {!radarrHistory ? ( ) : radarrHistory.length === 0 ? (

No recent activity

) : (
{radarrHistory.slice(0, 8).map((item, i) => (

{item.title}

{item.event} {item.quality} {formatShortDate(item.date)}
))}
)}
{/* Media Servers -- 2 columns */}
{/* Jellyfin */} Jellyfin {jellyfin && ( )} {!jellyfin ? ( ) : ( <>

{jellyfin.server_name}

Libraries

{jellyfin.libraries.map((lib) => (
{lib.name} {lib.type || "library"}
))}
{jellyfin.idle_sessions > 0 && (

{jellyfin.idle_sessions} idle session{jellyfin.idle_sessions > 1 ? "s" : ""}

)} )}
{/* Plex */} Plex {!plex ? ( ) : ( plex.servers.map((server) => (
{server.name}
{server.online && server.libraries.length > 0 && (
{server.libraries.map((lib, j) => (
{lib.title} {lib.type}
))}
)} {server.error && (

{server.error}

)}
)) )}
{/* Downloads & Services */}
{/* Sonarr Queue */} Sonarr Queue {!sonarr ? ( ) : sonarr.length === 0 ? ( ) : (
{sonarr.map((item, i) => (

{item.title}

{item.timeleft && ( {item.timeleft} )}
))}
)}
{/* Radarr Queue */} Radarr Queue {!radarr ? ( ) : radarr.length === 0 ? ( ) : (
{radarr.map((item, i) => (

{item.title}

{item.timeleft && ( {item.timeleft} )}
))}
)}
{/* SABnzbd + Deluge combined Downloads card */} Downloads
{sab && ( )} {deluge && ( )}
{/* SABnzbd */}

SABnzbd

{!sab ? ( ) : sab.slots.length === 0 ? ( ) : (
{sab.slots.map((item, i) => (

{item.title}

{item.timeleft && {item.timeleft}}
{item.progress != null && (
)}
))}
)}
{/* Deluge */}

Deluge

{!deluge ? ( ) : !deluge.available ? (

{deluge.error ?? "Unreachable"}

) : (

{deluge.total}

Total

{deluge.downloading}

Downloading

{deluge.seeding}

Seeding

)}
{/* Prowlarr + Bazarr combined */} Indexers & Subtitles {/* Prowlarr */}

Prowlarr

{prowlarr && !prowlarr.error && ( )}
{!prowlarr ? ( ) : prowlarr.error ? (

{prowlarr.error}

) : (

{prowlarr.enabled}/{prowlarr.total} indexers enabled

{prowlarr.indexers.slice(0, 5).map((idx, i) => (
{idx.name} {idx.protocol}
))}
)}
{/* Bazarr */}

Bazarr

{bazarr && !bazarr.error && ( )}
{!bazarr ? ( ) : bazarr.error ? (

{bazarr.error}

) : (
Wanted episodes {bazarr.wanted_episodes}
Wanted movies {bazarr.wanted_movies}
SignalR
)}
{/* Audiobookshelf */} Audiobookshelf {abs && !abs.error && ( {abs.total} items )} {!abs ? ( ) : abs.error ? (

{abs.error}

) : (
{abs.libraries.map((lib, i) => (
{lib.name}
{lib.items} {lib.type}
))}
)}
); }