fix(494): reconcile removed music videos in Jellyfin scanner #495

Merged
timothy merged 2 commits from feat/494-musicvideo-reconcile into main 2026-07-20 17:22:15 +02:00
2 Commits
Author SHA1 Message Date
timothyandClaude Opus 4.8 fdef908929 test(494): mixed-library zero-incoming regression + tighten removal test
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 16s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m50s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 20s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 21s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m38s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m42s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Address the #493 session's field finding: a mixed Jellyfin library runs the
music-video arm with a legitimately empty incoming set on every scan while
Movies/Shows exist under the same LibraryPath (the real "Standup" case). Add
ScanLibrary_Should_Not_Touch_Movies_Or_Shows_When_No_MusicVideos_Present — seeds
a Movie (with a real MediaFile path) + Show, scans with zero incoming, asserts
both survive and RemoveMediaItems is never called. This pins that the
MusicVideo-joined FindMusicVideoPaths (not just the empty-fetch guard) is what
keeps existing.Except([]) from wiping the movies/episodes.

Also tighten the removal test to pin the trashed music-video id in the
RemoveMediaItems assertion (cold-review nit), and name the deferred per-library
identity follow-up (#496) in decisions.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 17:02:58 +02:00
timothyandClaude Opus 4.8 2351841ff4 fix(494): reconcile removed music videos in Jellyfin scanner
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 1m29s
Build ErsatzTV Image / decisions.md append-only (pull_request) Failing after 13m45s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 16m21s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m58s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
JellyfinMusicVideoLibraryScanner did add/update only, so a music video (or
artist) deleted on the Jellyfin side lingered in ErsatzTV forever and could
still be scheduled. Add a trash sweep at the end of ScanLibrary
(TrashMissingMusicVideos) mirroring the MediaServer* base scanners' "gone
upstream => remove" pattern.

Identity is library-scoped by (LibraryPathId, path) rather than server itemId:
music videos carry no JellyfinMusicVideo entity / ItemId, so the sweep diffs the
incoming local paths against FindMusicVideoPaths(libraryPath) and hard-deletes
the remainder via DeleteByPath + RemoveMediaItems, then cleans now-empty artists
via DeleteEmptyArtists. Both repo queries filter LibraryPathId AND join the
concrete MusicVideo table, so a Movie/Show sharing the same LibraryPath (a mixed
library) can never be cross-deleted. Gated by the #477 MediaServerReconciliationGuard
so a successful-but-empty fetch does not wipe the library.

Known limitation: MusicVideoRepository.GetOrAdd dedups paths globally, so a file
served by two libraries with overlapping local paths is a single row owned by the
first library; a proper fix needs per-library music-video identity (deferred).

Integration tests (real repos over in-memory SQLite) pin removal, empty-artist
cleanup, cross-kind safety, and the empty-fetch guard. decisions.md records the
identity choice and the deferred follow-up.

fixes #494

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 16:55:51 +02:00