test(924): pin the deep-scan and no-stored-streams arms of the music-video statistics refresh
A clause sweep showed `deepScan ||` survived (the deep-scan test only ran against an empty stream list, which the streams arm covers) and that `result.IsAdded ||` is implied by the empty-streams arm while the projection carries no streams. The deep-scan test now re-reads statistics for an item that already has them; a returning-Unavailable test covers the streams arm alone. IsAdded is kept for parity with the movie scanner and says so. Refs #924 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: 99d1dedb-f027-4e05-baab-2111f3a7d951
This commit is contained in:
@@ -323,7 +323,9 @@ public abstract class MediaServerMusicVideoLibraryScanner<TConnectionParameters,
|
||||
// IMetadataRepository.UpdateStatistics reconciles streams against the version it is given, removed every
|
||||
// stream an existing row already had. Statistics come from the media server, under the same condition
|
||||
// MediaServerMovieLibraryScanner.UpdateStatistics uses; when the server cannot supply them nothing is written,
|
||||
// so stored streams are kept rather than replaced by the projection's empty list.
|
||||
// so stored streams are kept rather than replaced by the projection's empty list. (`result.IsAdded` is implied
|
||||
// by the empty-streams arm while the projection carries no streams; it is kept so the condition stays the
|
||||
// movie scanner's.)
|
||||
private async Task<Either<BaseError, MediaItemScanResult<TMusicVideo>>> UpdateStatistics(
|
||||
TConnectionParameters connectionParameters,
|
||||
TLibrary library,
|
||||
|
||||
@@ -728,9 +728,8 @@ public class JellyfinMusicVideoLibraryScannerTests
|
||||
|
||||
// ersatztv#924: Jellyfin's library-items projection carries no streams and no width/height — those come only
|
||||
// from the per-item playback info. These run against the REAL MetadataRepository so the assertions read what
|
||||
// was persisted. Non-vacuous: restoring the scanner's former
|
||||
// `UpdateStatistics(result.Item, incoming.GetHeadVersion())` reddens the three `..._Statistics_...` tests
|
||||
// below — the first two on the missing video stream, the third on the removed stream.
|
||||
// was persisted. Non-vacuous (measured 2026-10-21): restoring the scanner's former body,
|
||||
// `UpdateStatistics(result.Item, incoming.GetHeadVersion())`, reddens all five tests below.
|
||||
[Test]
|
||||
public async Task ScanLibrary_Should_Store_PlaybackInfo_Statistics_For_A_New_MusicVideo()
|
||||
{
|
||||
@@ -785,11 +784,14 @@ public class JellyfinMusicVideoLibraryScannerTests
|
||||
|
||||
var playbackCalls = 0;
|
||||
var serverAnswers = false;
|
||||
(int Width, int Height) size = (1280, 720);
|
||||
IJellyfinApiClient api = FakeApiWithPlaybackInfo(
|
||||
_ =>
|
||||
{
|
||||
playbackCalls++;
|
||||
return serverAnswers ? PlaybackInfo(1280, 720) : BaseError.New("playback info unavailable");
|
||||
return serverAnswers
|
||||
? PlaybackInfo(size.Width, size.Height)
|
||||
: BaseError.New("playback info unavailable");
|
||||
},
|
||||
() => BuildIncoming(VideoPath, "Artist 1", "Song 1"));
|
||||
|
||||
@@ -817,6 +819,51 @@ public class JellyfinMusicVideoLibraryScannerTests
|
||||
version.Streams.Count(s => s.MediaStreamKind == MediaStreamKind.Video).ShouldBe(1);
|
||||
version.Width.ShouldBe(1280);
|
||||
version.Height.ShouldBe(720);
|
||||
|
||||
// 4. a deep scan re-reads them even when streams are already stored and the etag is unchanged
|
||||
size = (1920, 1080);
|
||||
(await scanner.ScanLibrary(ConnectionParameters, library, deepScan: true, CancellationToken.None))
|
||||
.IsRight.ShouldBeTrue();
|
||||
playbackCalls.ShouldBe(3);
|
||||
(await HeadVersionOf(VideoPath)).Width.ShouldBe(1920);
|
||||
}
|
||||
|
||||
// The "no stored streams" arm of the refresh condition: an Unavailable item whose file is back is scanned with
|
||||
// an UNCHANGED etag and no deep scan, so only the empty stream list says its statistics must be read.
|
||||
[Test]
|
||||
public async Task ScanLibrary_Should_Read_Statistics_For_A_Returning_Unavailable_MusicVideo_Without_Streams()
|
||||
{
|
||||
int libraryPathId = await SeedLibraryPath("/data/music");
|
||||
JellyfinLibrary library = BuildLibrary(libraryPathId, "/data/music");
|
||||
const string VideoPath = "/data/music/artist1/song1.mkv";
|
||||
|
||||
var serverAnswers = false;
|
||||
IJellyfinApiClient api = FakeApiWithPlaybackInfo(
|
||||
_ => serverAnswers ? PlaybackInfo(1280, 720) : BaseError.New("playback info unavailable"),
|
||||
() => BuildIncoming(VideoPath, "Artist 1", "Song 1"));
|
||||
|
||||
(JellyfinMusicVideoLibraryScanner scanner, _) =
|
||||
BuildScanner(api, metadataRepository: new MetadataRepository(_db.Factory));
|
||||
|
||||
(await scanner.ScanLibrary(ConnectionParameters, library, deepScan: false, CancellationToken.None))
|
||||
.IsRight.ShouldBeTrue();
|
||||
|
||||
int musicVideoId = await MusicVideoId(VideoPath);
|
||||
await using (TvContext context = _db.CreateContext())
|
||||
{
|
||||
MediaItem mediaItem = await context.MediaItems.SingleAsync(mi => mi.Id == musicVideoId);
|
||||
mediaItem.State = MediaItemState.Unavailable;
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
serverAnswers = true;
|
||||
(await scanner.ScanLibrary(ConnectionParameters, library, deepScan: false, CancellationToken.None))
|
||||
.IsRight.ShouldBeTrue();
|
||||
|
||||
MediaVersion version = await HeadVersionOf(VideoPath);
|
||||
version.Streams.Count(s => s.MediaStreamKind == MediaStreamKind.Video).ShouldBe(1);
|
||||
version.Width.ShouldBe(1280);
|
||||
(await MediaItemStateOf(musicVideoId)).ShouldBe(MediaItemState.Normal);
|
||||
}
|
||||
|
||||
// When the server cannot supply statistics on a rescan, nothing is written: the projection's empty stream
|
||||
@@ -873,7 +920,7 @@ public class JellyfinMusicVideoLibraryScannerTests
|
||||
|
||||
// Refreshed statistics take their chapters from the INCOMING projection: the repository does not eager-load
|
||||
// chapters on an existing row, so JellyfinMovieLibraryScanner's source (the stored item) would have none to
|
||||
// write. This pins that choice; the former code also stored the projection's chapters, so it is green there.
|
||||
// write.
|
||||
[Test]
|
||||
public async Task ScanLibrary_Should_Refresh_Statistics_And_Projection_Chapters_When_The_Etag_Changes()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user