Independent cold review (Codex) returned BLOCKED. Findings 1, 3 and 4 are fixed here;
each has a regression test proven non-vacuous by a negative control.
1. Blocker — the replaced local path was discarded. The scanner computed localPath but
GetOrAdd only received `incoming`, so the repository re-derived the path from the
UNREPLACED projection. On any install with path replacements, adoption hashed the
server-side path, missed the existing row, ALSO slipped past MediaFileAlreadyExists
(which hashes that same wrong string) and inserted a duplicate row under a server path,
leaving the original collection-linked row identity-less forever. The test harness hid
this because its path-replacement stub was an identity function.
→ GetOrAdd now takes localPath explicitly and never reads the projection's path;
BuildPathReplacement takes a real mapping and the new test genuinely replaces.
3. Medium — GetByItemId matched on ItemId alone, so two media sources presenting the same
item id (cloned Jellyfin DB) resolved to each other's row, letting one library repoint
another's. → filtered by LibraryPath.LibraryId.
4. Medium — a row predating the identity that the server had ALREADY stopped reporting was
never adopted (adoption only runs for an incoming item) and carried no identity, so the
itemId diff could not see it either: it sat Normal and schedulable forever, strictly
worse than the hard delete it replaced. → GetExistingLegacyMusicVideoPaths +
FlagFileNotFoundByPaths reconcile legacy rows by local path, and they are counted into
the #477 empty-fetch guard (on the first scan after this ships they ARE the whole
library, so a guard counting only identity rows would sweep all of them on a transient
empty fetch).
Finding 2 (the issue's Done-when #2) is a scope question, not a defect, and is unchanged:
one file path is still one MediaItem row globally, so this lands music videos at parity
with movies rather than eliminating shared-row trashing. Recorded honestly in the decision
record; raised for an explicit call before the issue is closed.
fixes #496
38 lines
1.9 KiB
C#
38 lines
1.9 KiB
C#
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Metadata;
|
|
|
|
namespace ErsatzTV.Core.Interfaces.Repositories;
|
|
|
|
public interface IMediaServerMusicVideoRepository<in TLibrary, TMusicVideo, TEtag> where TLibrary : Library
|
|
where TMusicVideo : MusicVideo
|
|
where TEtag : MediaServerItemEtag
|
|
{
|
|
Task<List<TEtag>> GetExistingMusicVideos(TLibrary library);
|
|
Task<Option<int>> FlagNormal(TLibrary library, TMusicVideo musicVideo);
|
|
Task<Option<int>> FlagUnavailable(TLibrary library, TMusicVideo musicVideo);
|
|
Task<List<int>> FlagFileNotFound(TLibrary library, List<string> musicVideoItemIds);
|
|
|
|
// Unlike the movie/other-video seams, GetOrAdd takes the resolved Artist and LibraryFolder: a music video is
|
|
// owned by an Artist (FK, not null) and ersatztv#488 requires the media file to carry its LibraryFolderId.
|
|
//
|
|
// `localPath` is the PATH-REPLACED local path and is the only path this seam may store or match on. The
|
|
// `item` argument still carries the path the media server reported, which for any install with configured
|
|
// path replacements is a DIFFERENT string — matching or storing that one would miss the row it is meant to
|
|
// find and duplicate it under a server-side path (ersatztv#496).
|
|
Task<Either<BaseError, MediaItemScanResult<TMusicVideo>>> GetOrAdd(
|
|
TLibrary library,
|
|
Artist artist,
|
|
LibraryFolder libraryFolder,
|
|
TMusicVideo item,
|
|
string localPath,
|
|
bool deepScan,
|
|
CancellationToken cancellationToken);
|
|
|
|
// ersatztv#496: music videos that predate per-item identity carry no TEtag row, so they are invisible to the
|
|
// itemId diff above. They are reconciled by their local path until a scan adopts them.
|
|
Task<List<string>> GetExistingLegacyMusicVideoPaths(TLibrary library);
|
|
Task<List<int>> FlagFileNotFoundByPaths(TLibrary library, List<string> localPaths);
|
|
|
|
Task<Unit> SetEtag(TMusicVideo musicVideo, string etag);
|
|
}
|