diff --git a/apps/admin-ui/src/app/downloads/page.tsx b/apps/admin-ui/src/app/downloads/page.tsx index 8606133..a97f895 100644 --- a/apps/admin-ui/src/app/downloads/page.tsx +++ b/apps/admin-ui/src/app/downloads/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import Link from "next/link"; import { Nav, useAuth } from "@/components/Nav"; interface NodeRow { @@ -10,17 +11,8 @@ interface NodeRow { } interface DsConfig { - id?: string; nodeId: string; nodeName?: string; - baseUrl: string; - username: string; - downloadDestination: string; - downloadHostPath: string; - fourKHostPath: string; - enabled: boolean; - lastOkAt?: string | null; - lastError?: string | null; } interface SearchHit { @@ -61,13 +53,7 @@ interface JobRow { movieTitle: string; movieYear: number | null; posterUrl: string | null; - tmdbId: number | null; - imdbId: string | null; - destFolderName: string | null; - importedPath: string | null; error: string | null; - createdAt: string; - updatedAt: string; } function formatBytes(n: number): string { @@ -97,26 +83,28 @@ function statusClass(s: string): string { } } -const emptyConfig = (nodeId = ""): DsConfig => ({ - nodeId, - baseUrl: "", - username: "", - downloadDestination: "Downloads", - downloadHostPath: "/volume1/Downloads", - fourKHostPath: "/volume1/Media/Films/4K", - enabled: true, -}); +function statusLabel(s: string): string { + switch (s) { + case "DOWNLOADING": + return "Downloaden"; + case "FINISHED": + return "Klaar"; + case "IMPORTING": + return "Importeren"; + case "IMPORTED": + return "Geïmporteerd"; + case "FAILED": + return "Mislukt"; + default: + return s; + } +} export default function DownloadsPage() { useAuth(); const [nodes, setNodes] = useState([]); const [configs, setConfigs] = useState([]); const [nodeId, setNodeId] = useState(""); - const [cfg, setCfg] = useState(emptyConfig()); - const [password, setPassword] = useState(""); - const [cfgMsg, setCfgMsg] = useState(null); - const [cfgErr, setCfgErr] = useState(null); - const [cfgBusy, setCfgBusy] = useState(false); const [query, setQuery] = useState(""); const [searching, setSearching] = useState(false); @@ -145,7 +133,7 @@ export default function DownloadsPage() { .catch(() => undefined); }, []); - const loadAll = useCallback(() => { + const loadBootstrap = useCallback(() => { Promise.all([ fetch("/api/v1/admin/nodes", { credentials: "include" }).then((r) => r.json()), fetch("/api/v1/admin/downloads/configs", { credentials: "include" }).then((r) => r.json()), @@ -160,56 +148,22 @@ export default function DownloadsPage() { setNodes(nodeList); const list: DsConfig[] = c.configs ?? []; setConfigs(list); - const preferred = list[0]?.nodeId || nodeList[0]?.id || ""; - setNodeId((prev) => prev || preferred); + setNodeId((prev) => prev || list[0]?.nodeId || nodeList[0]?.id || ""); }); loadJobs(); }, [loadJobs]); useEffect(() => { - loadAll(); + loadBootstrap(); const t = setInterval(loadJobs, 15_000); return () => clearInterval(t); - }, [loadAll, loadJobs]); - - useEffect(() => { - if (!nodeId) return; - const existing = configs.find((c) => c.nodeId === nodeId); - setCfg(existing ? { ...existing } : emptyConfig(nodeId)); - setPassword(""); - setCfgMsg(null); - setCfgErr(null); - }, [nodeId, configs]); + }, [loadBootstrap, loadJobs]); const hasConfig = useMemo(() => configs.some((c) => c.nodeId === nodeId), [configs, nodeId]); - - async function saveConfig() { - setCfgBusy(true); - setCfgMsg(null); - setCfgErr(null); - try { - const res = await fetch("/api/v1/admin/downloads/config", { - method: "PUT", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - ...cfg, - nodeId, - password: password || undefined, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) { - setCfgErr(data.error?.message ?? "Opslaan mislukt"); - return; - } - setCfgMsg("Opgeslagen — verbinding OK"); - setPassword(""); - loadAll(); - } finally { - setCfgBusy(false); - } - } + const configuredNodes = useMemo(() => { + const ids = new Set(configs.map((c) => c.nodeId)); + return nodes.filter((n) => ids.has(n.id)); + }, [nodes, configs]); async function runSearch() { if (!query.trim() || !nodeId) return; @@ -268,10 +222,7 @@ export default function DownloadsPage() { setSubs([]); setSelectedSubId(null); try { - const params = new URLSearchParams({ - languages: "nl", - releaseHint, - }); + const params = new URLSearchParams({ languages: "nl", releaseHint }); if (meta.imdbId) params.set("imdbId", meta.imdbId); else params.set("tmdbId", String(meta.tmdbId)); const res = await fetch(`/api/v1/admin/downloads/subtitles?${params}`, { @@ -284,7 +235,7 @@ export default function DownloadsPage() { } setSubsConfigured(data.configured !== false); if (data.configured === false) { - setSubsMsg("OpenSubtitles API-key niet geconfigureerd op Master"); + setSubsMsg("OpenSubtitles staat uit — zet aan onder Instellingen"); return; } const results: SubtitleHit[] = data.results ?? []; @@ -354,7 +305,7 @@ export default function DownloadsPage() { return ( <> -
-

Plank toevoegen

-
- setNewShelfName(e.target.value)} - placeholder="bijv. Kids Films" - /> - - + {tab === "shelves" && ( +
+
+

Bibliotheek-planken

+

+ Namen zoals Kids Films of NL Series. Op Nodes koppel je scanpaden aan een plank. +

-
-
-

Bestaande planken

+
+

Plank toevoegen

+
+ setNewShelfName(e.target.value)} + placeholder="bijv. Kids Films" + /> + + +
+
-
- - - - - - - - - - - - {shelves.map((s) => ( - - - - - - - - ))} - {shelves.length === 0 && ( +
+

Bestaande planken

+
+
NaamTypePad(en)Bestanden
{s.name}{s.kind === "MOVIE" ? "Films" : "Series"} - {s.paths?.length ? ( - s.paths.map((p) => ( -
- {p.nodeName}: {p.path} -
- )) - ) : ( - {s.scanRootCount} pad(en) - )} -
- {s.mediaFileCount} - {s.scanRootCount > 0 && s.mediaFileCount === 0 && ( -
Pad gekoppeld, nog geen scan
- )} -
- {s.slug !== "films" && s.slug !== "series" && ( - - )} -
+ - + + + + + - )} - -
- Laden… - NaamTypePad(en)Bestanden
-
+ + + {shelves.map((s) => ( + + {s.name} + {s.kind === "MOVIE" ? "Films" : "Series"} + + {s.paths?.length ? ( + s.paths.map((p) => ( +
+ {p.nodeName}: {p.path} +
+ )) + ) : ( + {s.scanRootCount} pad(en) + )} + + + {s.mediaFileCount} + {s.scanRootCount > 0 && s.mediaFileCount === 0 && ( +
Pad gekoppeld, nog geen scan
+ )} + + + {s.slug !== "films" && s.slug !== "series" && ( + + )} + + + ))} + {shelves.length === 0 && ( + + + Laden… + + + )} + + +
-
- {shelves.map((s) => ( -
-
- {s.name} - - {s.kind === "MOVIE" ? "Films" : "Series"} - -
-
- {s.mediaFileCount} bestanden - {s.scanRootCount} pad(en) -
- {s.paths?.length ? ( -
- {s.paths.map((p) => ( -
- {p.nodeName} - {p.path} -
- ))} +
+ {shelves.map((s) => ( +
+
+ {s.name} + + {s.kind === "MOVIE" ? "Films" : "Series"} +
- ) : null} - {s.scanRootCount > 0 && s.mediaFileCount === 0 && ( -
Pad gekoppeld, nog geen scan
- )} - {s.slug !== "films" && s.slug !== "series" && ( -
- +
+ {s.mediaFileCount} bestanden + {s.scanRootCount} pad(en)
- )} -
- ))} - {shelves.length === 0 &&

Laden…

} + {s.paths?.length ? ( +
+ {s.paths.map((p) => ( +
+ {p.nodeName} + {p.path} +
+ ))} +
+ ) : null} + {s.scanRootCount > 0 && s.mediaFileCount === 0 && ( +
Pad gekoppeld, nog geen scan
+ )} + {s.slug !== "films" && s.slug !== "series" && ( +
+ +
+ )} +
+ ))} + {shelves.length === 0 &&

Laden…

} +
- - + + )} -
-
-

OpenSubtitles

-

- Voor NL-ondertitels bij Downloads. Key aanmaken op opensubtitles.com → API consumers. - Gebruikersnaam/wachtwoord verhogen de downloadlimiet. -

-
-
-
- - - - + {tab === "download-station" && ( +
+
+

Download Station

+

+ Synology DS-login en vaste paden voor 4K-import. Zoeken doe je onder{" "} + Downloads. +

-
- - - Status: {osConfigured ? "geconfigureerd" : "niet geconfigureerd"} - - {osMsg && {osMsg}} - {osErr && {osErr}} +
+
+ +
+
+ + + + + + +
+
+ + {cfgMsg && {cfgMsg}} + {cfgErr && {cfgErr}} +
+ {cfg.lastError &&

Laatste fout: {cfg.lastError}

}
-
-
+
+ )} + + {tab === "opensubtitles" && ( +
+
+

OpenSubtitles

+

+ NL-ondertitels bij Downloads. Key via opensubtitles.com → API consumers. +

+
+
+
+ + + + +
+
+ + + Status: {osConfigured ? "geconfigureerd" : "niet geconfigureerd"} + + {osMsg && {osMsg}} + {osErr && {osErr}} +
+
+
+ )} );