fix(496): harden the legacy path sweep against MySQL collation (re-review Low)

Re-review returned MERGEABLE with one Low, MySQL-collation-dependent edge in
FlagFileNotFoundByPaths: the C#-side Except diff is ordinal, but `MF.Path IN @LocalPaths`
runs under MySQL's case-insensitive default collation, so a still-reported identified row
differing only in case from an absent legacy row could be matched and flagged missing.

Match on the indexed PathHash instead of collated Path text, and re-state the
NOT EXISTS (JellyfinMusicVideo) guard so the identity pass and the legacy pass are disjoint
by construction rather than by the caller's diff being correct. SQLite was unaffected.
This commit is contained in:
2026-07-25 17:20:53 +02:00
parent 6bd1d954bd
commit 147166b053
2 changed files with 14 additions and 4 deletions
+3 -2
View File
@@ -3857,7 +3857,7 @@ This is a **second, independent** refusal on the same guard, plus a decision not
## 2026-07-25 — Music videos carry a per-library server identity; reconciliation is an itemId diff + soft trash (#496)
`key: scan.musicvideo-server-identity` · `status: active` · `since: 2026-07-25` · `supersedes: scan.musicvideo-reconciliation@2026-07-20` · `superseded-by: none`
**Rule:** Jellyfin music videos carry a per-library server identity (`JellyfinMusicVideo : MusicVideo` with `ItemId`/`Etag`, TPT table + ItemId index), so `JellyfinMusicVideoLibraryScanner` folds onto a shared `MediaServerMusicVideoLibraryScanner` base that diffs the **server item id** and soft-trashes (`FlagFileNotFound`) instead of diffing local paths and hard-deleting. Rows predating the identity are **adopted in place** — the identity row is inserted against the same `MediaItem` id, scoped to the scanned library's own `LibraryPath` — never deleted and re-added.
**Signals:** music-video server identity, JellyfinMusicVideo ItemId/Etag, itemId diff, soft FileNotFound trash, adoption of pre-identity rows, cross-library false-trash, path-replaced PathHash · paths: `JellyfinMusicVideo`, `JellyfinMusicVideoRepository`, `IMediaServerMusicVideoRepository`, `MediaServerMusicVideoLibraryScanner`, `JellyfinMusicVideoLibraryScanner` · issues: #496, #494, #477, #488, #497, #500
**Signals:** music-video server identity, JellyfinMusicVideo ItemId/Etag, itemId diff, soft FileNotFound trash, adoption of pre-identity rows, cross-library false-trash, path-replaced PathHash · paths: `JellyfinMusicVideo`, `JellyfinMusicVideoRepository`, `IMediaServerMusicVideoRepository`, `MediaServerMusicVideoLibraryScanner`, `JellyfinMusicVideoLibraryScanner` · issues: #496, #494, #477, #488, #497, #500, #606
**Mechanics:** dual-provider migration `Add_JellyfinMusicVideo`; `GetOrAdd` takes `localPath` explicitly and never reads the projection's path; adoption probe joins `MediaFile.PathHash` + `NOT EXISTS (JellyfinMusicVideo)` filtered to the library's `LibraryPath`; `GetByItemId` filters `LibraryPath.LibraryId`; legacy (identity-less) rows reconciled by `GetExistingLegacyMusicVideoPaths` + `FlagFileNotFoundByPaths` and counted into the #477 guard; `ScanLibrary_Should_Adopt_Using_The_PathReplaced_Local_Path`, `ScanLibrary_Should_Adopt_PreExisting_MusicVideo_Preserving_Identity_And_Collections`, `ScanLibrary_Should_Not_Adopt_A_MusicVideo_Owned_By_Another_LibraryPath`, `ScanLibrary_Should_Not_Resolve_An_ItemId_Owned_By_Another_Library`, `ScanLibrary_Should_Flag_A_Legacy_Row_The_Server_No_Longer_Reports`
#494 gave music videos a trash sweep but had to key it on `(LibraryPathId, path)` and hard-delete, because
@@ -3904,4 +3904,5 @@ owner's sweep **destroyed** the row the other library still served. This is the
(`MediaFileAlreadyExists` is a global path-hash guard), so a second library serving the same file still gets
no row of its own — exactly as for movies/TV. What changes is that the first library's sweep now *flags*
rather than *destroys* that shared row. The unrecoverable data loss is gone; the shared-row limitation is a
whole-app property, not a music-video one.
whole-app property, not a music-video one, and is tracked separately as **#606**. #496's Done-when was
amended to this parity wording rather than ticked as literally written.