389 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|