Add confirm+stop to Library Cleanup scan; branded album art placeholder

- Library Cleanup scan is now a real background job (POST .../start,
  GET .../status, POST .../stop) instead of one long synchronous request,
  so a Stop button can actually abandon it instead of just waiting.
  Frontend confirms before starting (explains it can take a minute+)
  and polls status while running.
- New shared AlbumArt component: shows real art when available, otherwise
  a branded "Streamhoard" placeholder instead of a bare icon in an empty
  box. Used in the Music grid, album detail header, and the player bar —
  swaps to real art automatically once enrichment/a manual edit sets it.
This commit is contained in:
Myron Blair
2026-07-26 23:15:07 -05:00
parent 43340c0f49
commit cf8c7890be
6 changed files with 132 additions and 28 deletions
+39 -3
View File
@@ -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")