- Navbar: drop the Users link (already reachable from Admin's quick-links)
- Admin quick-links: drop Settings (already reachable from the navbar
for every user)
- library_cleanup.py: parallelize the missing-file stat checks across a
small thread pool instead of one file at a time — sequential NAS round
trips were taking minutes for a library of any real size, even after
moving them off the event loop.
scan() was doing hundreds of blocking Path.is_file()/stat() calls directly
in an async function -- movie/episode/track files often live on the
NFS/CIFS-mounted NAS, so each stat call has real network latency. Since
the backend runs a single-threaded asyncio event loop, this froze every
other request (confirmed live: a plain GET /api/movies hung for 60s+ while
a cleanup scan was in flight) for as long as the scan took.
Moved every filesystem-touching check (missing files, orphaned HLS dirs)
into a plain sync function run via asyncio.to_thread, so the scan can no
longer block anything else.
- Episode transcode (single + bulk) now accepts storage_type=scan, matching
stream_episode and the movie-transcode endpoint — Library-Scan-imported
TV episodes can finally be transcoded, not just streamed.
- retry_job now forwards content_type, fixing retry for failed/cancelled
episode jobs (was silently treating them as movies and failing again).
- cancel_job now updates the correct collection (movies vs episodes) based
on content_type instead of always writing to db.movies.
- approve_request atomically claims the request (pending -> approving)
before contacting Radarr/Sonarr, so a double-click or two concurrent
admins can't both add the same content; releases the claim back to
pending on any failure so a fixable error can be retried.
- Music enrichment's update_many now re-checks album_art_url is still
empty, so it no longer clobbers art an admin manually set via PATCH.
- Library Cleanup's dangling-refs scan now also checks episode_progress
for rows referencing a deleted episode (previously only watchlist/
progress against movie_id were checked).
- Settings now surfaces a failed bulk-TMDB-enrich run as an error instead
of a false "Enrichment done" success summary.
- Music player's isPlaying now syncs from the audio element's native
play/pause events instead of only being set inside togglePlay/track-
change, so it can't drift from actual playback state (buffering stalls,
OS media-key pauses, etc).
Fixing a missing-file or duplicate-import episode only deleted the
episode itself, leaving its episode_progress/subtitle rows behind for
the next scan to catch as separate orphaned_subtitles/dangling findings.
Consolidated movie/episode/track deletion (with all their dependent rows)
into one _delete_content() helper, used consistently by missing_files,
orphaned_episodes, and duplicate_paths.
New read-only scan (GET /api/library/cleanup/scan) across movies, episodes,
tracks, subtitles, HLS output, transcode queue, and watchlist/progress rows:
- Missing files: DB record exists but the file is gone from disk
- Orphaned episodes: show_id no longer exists
- Orphaned HLS output: transcoded folders left behind by deleted content
- Stuck transcode jobs: "running" 2+ hours with no completion (backend-restart artifact)
- Duplicate imports: same file imported into the library more than once
- Orphaned subtitles / dangling watchlist+progress rows
Nothing is deleted until an admin clicks Fix (per-item or Fix All per
category) via POST /api/library/cleanup/fix.