diff --git a/backend/server.py b/backend/server.py
index a198694..edf9d4d 100644
--- a/backend/server.py
+++ b/backend/server.py
@@ -1537,9 +1537,45 @@ async def scan_import(payload: dict, user: dict = Depends(require_admin)):
# ============ LIBRARY CLEANUP (admin) ============
-@api.get("/library/cleanup/scan")
-async def library_cleanup_scan(user: dict = Depends(require_admin)):
- return await library_cleanup.scan(db, VIDEOS_DIR, HLS_DIR)
+_cleanup_scan_status = {"running": False, "findings": None, "error": "", "scan_id": 0}
+
+
+async def _run_cleanup_scan(scan_id: int):
+ try:
+ findings = await library_cleanup.scan(db, VIDEOS_DIR, HLS_DIR)
+ if _cleanup_scan_status["scan_id"] == scan_id:
+ _cleanup_scan_status["findings"] = findings
+ except Exception as e:
+ if _cleanup_scan_status["scan_id"] == scan_id:
+ _cleanup_scan_status["error"] = str(e)
+ finally:
+ if _cleanup_scan_status["scan_id"] == scan_id:
+ _cleanup_scan_status["running"] = False
+
+
+@api.post("/library/cleanup/scan/start")
+async def library_cleanup_scan_start(user: dict = Depends(require_admin)):
+ if _cleanup_scan_status["running"]:
+ raise HTTPException(status_code=409, detail="Scan already running")
+ _cleanup_scan_status["scan_id"] += 1
+ _cleanup_scan_status.update(running=True, findings=None, error="")
+ asyncio.create_task(_run_cleanup_scan(_cleanup_scan_status["scan_id"]))
+ return {"ok": True}
+
+
+@api.get("/library/cleanup/scan/status")
+async def library_cleanup_scan_status(user: dict = Depends(require_admin)):
+ return _cleanup_scan_status
+
+
+@api.post("/library/cleanup/scan/stop")
+async def library_cleanup_scan_stop(user: dict = Depends(require_admin)):
+ # The already-dispatched file checks finish naturally in the background (there's no clean
+ # way to interrupt a ThreadPoolExecutor mid-flight), but bumping scan_id means whenever they
+ # do finish, _run_cleanup_scan sees a stale id and discards the result instead of surfacing it.
+ _cleanup_scan_status["scan_id"] += 1
+ _cleanup_scan_status["running"] = False
+ return {"ok": True}
@api.post("/library/cleanup/fix")
diff --git a/frontend/src/components/AlbumArt.jsx b/frontend/src/components/AlbumArt.jsx
new file mode 100644
index 0000000..92bb9d7
--- /dev/null
+++ b/frontend/src/components/AlbumArt.jsx
@@ -0,0 +1,20 @@
+// Shows real album art when a URL is available; otherwise a branded placeholder instead of an
+// empty box. Once musicbrainz enrichment (or a manual edit) sets album_art_url, callers just
+// pass the new url and this swaps over automatically — no separate "loaded" state to manage.
+export default function AlbumArt({ url, size = "lg", className = "" }) {
+ if (url) {
+ return ;
+ }
+ const small = size === "sm";
+ return (
+
{current.title}
diff --git a/frontend/src/pages/AlbumDetail.jsx b/frontend/src/pages/AlbumDetail.jsx index 1d749cb..10db63c 100644 --- a/frontend/src/pages/AlbumDetail.jsx +++ b/frontend/src/pages/AlbumDetail.jsx @@ -3,6 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import api from "../lib/api"; import { Play, Pause, ArrowLeft, Music2 } from "lucide-react"; import { useMusicPlayer } from "../lib/musicPlayer"; +import AlbumArt from "../components/AlbumArt"; export default function AlbumDetail() { const { artist, album } = useParams(); @@ -40,8 +41,8 @@ export default function AlbumDetail() {{a.album}
{a.artist} · {a.count} tracks