import { useEffect, useRef, useState } from "react"; import api from "../lib/api"; import { toast } from "sonner"; import { RefreshCcw, Download, Check, Sparkles, CheckSquare, Square } from "lucide-react"; const KINDS = [ { key: "movies", label: "Movies" }, { key: "tv", label: "TV Shows" }, { key: "music", label: "Music" }, ]; export default function LibraryScan() { const [kind, setKind] = useState("movies"); const [results, setResults] = useState([]); const [selected, setSelected] = useState(new Set()); const [loading, setLoading] = useState(false); const [importing, setImporting] = useState(false); const [enrich, setEnrich] = useState(null); const requestSeq = useRef(0); const load = async (k = kind) => { const seq = ++requestSeq.current; setLoading(true); setSelected(new Set()); try { const { data } = await api.get(`/scan/${k}`); if (seq !== requestSeq.current) return; // a newer tab switch/rescan superseded this response setResults(data); } catch (err) { if (seq !== requestSeq.current) return; toast.error(err.response?.data?.detail || "Scan failed"); } finally { if (seq === requestSeq.current) setLoading(false); } }; useEffect(() => { setResults([]); load(kind); }, [kind]); useEffect(() => { if (!enrich?.running) return; const t = setInterval(async () => { const { data } = await api.get("/music/enrich/status"); setEnrich(data); if (!data.running) clearInterval(t); }, 2000); return () => clearInterval(t); }, [enrich?.running]); const toggle = (path) => { const s = new Set(selected); s.has(path) ? s.delete(path) : s.add(path); setSelected(s); }; const importable = results.filter((r) => !r.already_imported); const allSelected = importable.length > 0 && importable.every((r) => selected.has(r.storage_path)); const toggleSelectAll = () => { setSelected(allSelected ? new Set() : new Set(importable.map((r) => r.storage_path))); }; const importSelected = async () => { if (selected.size === 0) return; setImporting(true); try { const items = results.filter((r) => selected.has(r.storage_path)); const { data } = await api.post("/scan/import", { kind, items }); toast.success(`Imported ${data.imported} item(s)`); load(); } catch (err) { toast.error(err.response?.data?.detail || "Import failed"); } finally { setImporting(false); } }; const startEnrich = async () => { try { await api.post("/music/enrich"); const { data } = await api.get("/music/enrich/status"); setEnrich(data); toast.success("Fetching metadata & covers from MusicBrainz…"); } catch (err) { toast.error(err.response?.data?.detail || "Could not start enrichment"); } }; const label = (r) => { if (kind === "movies") return `${r.title}${r.year ? ` (${r.year})` : ""}`; if (kind === "tv") return `${r.show_title} · S${r.season_number}E${r.episode_number ?? "?"} · ${r.title}`; return `${r.artist} — ${r.album} — ${r.title}`; }; return (
Filesystem

Library Scan

Scans the NAS mounts directly for movies, TV, and music that aren't already in the library — separate from Radarr/Sonarr, which are untouched.

{KINDS.map((k) => ( ))}
{kind === "music" && ( )}

{loading ? "Scanning…" : `${importable.length} new of ${results.length} found on disk.`}

{results.map((r) => ( ))} {results.length === 0 && !loading && (
Nothing found on disk for this category.
)}
); }