Files
homelab-optimized/dashboard/ui/app/media/page.tsx
Gitea Mirror Bot 2be8f1fe17
Some checks failed
Documentation / Build Docusaurus (push) Failing after 5m1s
Documentation / Deploy to GitHub Pages (push) Has been skipped
Sanitized mirror from private repository - 2026-04-05 08:31:50 UTC
2026-04-05 08:31:50 +00:00

389 lines
14 KiB
TypeScript

"use client";
import { usePoll } from "@/lib/use-poll";
import { JellyfinCard } from "@/components/jellyfin-card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { StatusBadge } from "@/components/status-badge";
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 ProwlarrIndexer {
name: string;
protocol: string;
}
interface ProwlarrStats {
total: number;
enabled: number;
indexers: ProwlarrIndexer[];
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;
}
export default function MediaPage() {
const { data: sonarrRaw } = usePoll<Record<string, unknown>>("/api/sonarr/queue", 30000);
const { data: radarrRaw } = usePoll<Record<string, unknown>>("/api/radarr/queue", 30000);
const { data: sabRaw } = usePoll<Record<string, unknown>>("/api/sabnzbd/queue", 30000);
const { data: prowlarr } = usePoll<ProwlarrStats>("/api/prowlarr/stats", 60000);
const { data: bazarr } = usePoll<BazarrStatus>("/api/bazarr/status", 60000);
const { data: abs } = usePoll<ABSStats>("/api/audiobookshelf/stats", 60000);
const { data: deluge } = usePoll<DelugeStatus>("/api/deluge/status", 30000);
const sonarr = (sonarrRaw?.records ?? sonarrRaw?.items ?? []) as SonarrQueueItem[];
const radarr = (radarrRaw?.records ?? radarrRaw?.items ?? []) as RadarrQueueItem[];
const sab = sabRaw?.queue as SabQueue | undefined;
return (
<div className="space-y-8">
<h1 className="text-lg font-semibold">Media</h1>
<JellyfinCard />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{/* Sonarr Queue */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Sonarr Queue</CardTitle>
</CardHeader>
<CardContent>
{!sonarr ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : sonarr.length === 0 ? (
<p className="text-xs text-muted-foreground/60">Queue empty</p>
) : (
<div className="space-y-2">
{sonarr.map((item, i) => (
<div key={i} className="text-xs space-y-0.5 rounded-lg px-2 py-1.5 hover:bg-white/[0.03] transition-colors">
<p className="text-foreground font-medium truncate">
{item.title}
</p>
<div className="flex items-center gap-2">
<StatusBadge
color={
item.status === "completed"
? "green"
: item.status === "downloading"
? "blue"
: "amber"
}
label={item.status}
/>
{item.timeleft && (
<span className="text-muted-foreground/70">
{item.timeleft}
</span>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Radarr Queue */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Radarr Queue</CardTitle>
</CardHeader>
<CardContent>
{!radarr ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : radarr.length === 0 ? (
<p className="text-xs text-muted-foreground/60">Queue empty</p>
) : (
<div className="space-y-2">
{radarr.map((item, i) => (
<div key={i} className="text-xs space-y-0.5 rounded-lg px-2 py-1.5 hover:bg-white/[0.03] transition-colors">
<p className="text-foreground font-medium truncate">
{item.title}
</p>
<div className="flex items-center gap-2">
<StatusBadge
color={
item.status === "completed"
? "green"
: item.status === "downloading"
? "blue"
: "amber"
}
label={item.status}
/>
{item.timeleft && (
<span className="text-muted-foreground/70">
{item.timeleft}
</span>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* SABnzbd Queue */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">
SABnzbd Queue
</CardTitle>
{sab && (
<StatusBadge
color={sab.paused ? "amber" : "green"}
label={sab.paused ? "Paused" : sab.speed}
/>
)}
</CardHeader>
<CardContent>
{!sab ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : sab.slots.length === 0 ? (
<p className="text-xs text-muted-foreground/60">Queue empty</p>
) : (
<div className="space-y-2">
{sab.slots.map((item, i) => (
<div key={i} className="text-xs space-y-1 rounded-lg px-2 py-1.5 hover:bg-white/[0.03] transition-colors">
<p className="text-foreground font-medium truncate">
{item.title}
</p>
<div className="flex items-center gap-2">
<StatusBadge
color={
item.status === "Downloading" ? "blue" : "amber"
}
label={item.status}
/>
{item.timeleft && (
<span className="text-muted-foreground/70">
{item.timeleft}
</span>
)}
</div>
{item.progress != null && (
<div className="glass-bar-track h-1">
<div
className="h-full glass-bar-fill bg-gradient-to-r from-blue-500 to-cyan-400"
style={{ width: `${item.progress}%` }}
/>
</div>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* Arr Suite Services */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
{/* Prowlarr */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Prowlarr</CardTitle>
{prowlarr && !prowlarr.error && (
<StatusBadge color="green" label={`${prowlarr.enabled} active`} />
)}
</CardHeader>
<CardContent>
{!prowlarr ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : prowlarr.error ? (
<p className="text-xs text-red-400">{prowlarr.error}</p>
) : (
<div className="space-y-2">
<p className="text-xs text-muted-foreground/70">
{prowlarr.enabled}/{prowlarr.total} indexers enabled
</p>
{prowlarr.indexers.map((idx, i) => (
<div key={i} className="flex items-center justify-between text-xs">
<span className="text-foreground truncate">{idx.name}</span>
<span className="text-muted-foreground/60 ml-2 shrink-0">
{idx.protocol}
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Bazarr */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Bazarr</CardTitle>
{bazarr && !bazarr.error && (
<StatusBadge color="green" label={bazarr.version} />
)}
</CardHeader>
<CardContent>
{!bazarr ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : bazarr.error ? (
<p className="text-xs text-red-400">{bazarr.error}</p>
) : (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground/70">Wanted episodes</span>
<span className="text-foreground font-medium">
{bazarr.wanted_episodes}
</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground/70">Wanted movies</span>
<span className="text-foreground font-medium">
{bazarr.wanted_movies}
</span>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground/70">Sonarr SignalR</span>
<StatusBadge
color={bazarr.sonarr_signalr === "LIVE" ? "green" : "red"}
label={String(bazarr.sonarr_signalr)}
/>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground/70">Radarr SignalR</span>
<StatusBadge
color={bazarr.radarr_signalr === "LIVE" ? "green" : "red"}
label={String(bazarr.radarr_signalr)}
/>
</div>
</div>
)}
</CardContent>
</Card>
{/* Audiobookshelf */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Audiobookshelf</CardTitle>
{abs && !abs.error && (
<StatusBadge color="green" label={`${abs.total} items`} />
)}
</CardHeader>
<CardContent>
{!abs ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : abs.error ? (
<p className="text-xs text-red-400">{abs.error}</p>
) : (
<div className="space-y-2">
{abs.libraries.map((lib, i) => (
<div key={i} className="flex items-center justify-between text-xs">
<span className="text-foreground">{lib.name}</span>
<span className="text-muted-foreground/70">
{lib.items} {lib.type}
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Deluge */}
<Card>
<CardHeader className="pb-2 flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Deluge</CardTitle>
{deluge && (
<StatusBadge
color={deluge.available ? "green" : "red"}
label={deluge.available ? "Online" : "Offline"}
/>
)}
</CardHeader>
<CardContent>
{!deluge ? (
<p className="text-xs text-muted-foreground">Loading...</p>
) : !deluge.available ? (
<p className="text-xs text-red-400">
{deluge.error ?? "Unreachable"}
</p>
) : (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground/70">Total</span>
<span className="text-foreground font-medium">
{deluge.total}
</span>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground/70">Downloading</span>
<StatusBadge
color={deluge.downloading ? "blue" : "green"}
label={String(deluge.downloading)}
/>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground/70">Seeding</span>
<StatusBadge
color={deluge.seeding ? "purple" : "green"}
label={String(deluge.seeding)}
/>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}