Sanitized mirror from private repository - 2026-04-05 08:31:50 UTC
This commit is contained in:
388
dashboard/ui/app/media/page.tsx
Normal file
388
dashboard/ui/app/media/page.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user