using System; using System.Collections.Generic; using System.Threading; using AutoFixture; using AutoFixture.AutoMoq; using Emby.Naming.Common; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using Moq; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library.LibraryManager; public sealed class ResolveAlternateVersionTests : IDisposable { private const string PrimaryPath = "/movies/Up/Up.mkv"; private const string AlternatePath = "/movies/Up/Up - 1080p.mkv"; private readonly Emby.Server.Implementations.Library.LibraryManager _libraryManager; private readonly Mock _persistenceServiceMock; private readonly Folder _staleParent; private readonly ILibraryManager? _previousLibraryManager; private readonly IMediaSourceManager? _previousMediaSourceManager; private readonly IItemRepository? _previousItemRepository; public ResolveAlternateVersionTests() { var fixture = new Fixture().Customize(new AutoMoqCustomization()); fixture.Register(() => new NamingOptions()); fixture.Freeze>() .Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data"); _persistenceServiceMock = fixture.Freeze>(); var itemRepositoryMock = fixture.Freeze>(); fixture.Freeze>() .Setup(f => f.GetFileInfo(It.IsAny())) .Returns(path => new FileSystemMetadata { FullName = path }); _libraryManager = fixture.Build() .Do(s => s.AddParts( fixture.Create>(), [], fixture.Create>(), fixture.Create>(), fixture.Create>())) .Create(); // BaseItem resolves these through process-wide statics; restored in Dispose. _previousLibraryManager = BaseItem.LibraryManager; _previousMediaSourceManager = BaseItem.MediaSourceManager; _previousItemRepository = BaseItem.ItemRepository; BaseItem.LibraryManager = _libraryManager; var mediaSourceManagerMock = new Mock(); mediaSourceManagerMock.Setup(m => m.GetMediaStreams(It.IsAny())).Returns([]); mediaSourceManagerMock.Setup(m => m.GetMediaAttachments(It.IsAny())).Returns([]); BaseItem.MediaSourceManager = mediaSourceManagerMock.Object; // A reloaded listing comes back empty, so a stale entry surviving is visible. itemRepositoryMock.Setup(i => i.GetItemList(It.IsAny())).Returns([]); BaseItem.ItemRepository = itemRepositoryMock.Object; BaseItem.FileSystem ??= fixture.Create(); BaseItem.MediaSegmentManager ??= fixture.Create(); BaseItem.ConfigurationManager ??= fixture.Create(); Video.RecordingsManager ??= fixture.Create(); var primary = new Movie { Name = "Up", Path = PrimaryPath, LocalAlternateVersions = [AlternatePath], Id = _libraryManager.GetNewItemId(PrimaryPath, typeof(Movie)) }; _staleParent = new Folder { Name = "Up", Path = "/movies/Up", Id = _libraryManager.GetNewItemId("/movies/Up", typeof(Folder)) }; var staleAlternate = new Video { Name = "Up - 1080p", Path = AlternatePath, OwnerId = primary.Id, ParentId = _staleParent.Id, Id = _libraryManager.GetNewItemId(AlternatePath, typeof(Video)) }; staleAlternate.SetPrimaryVersionId(primary.Id); itemRepositoryMock .Setup(i => i.RetrieveItem(It.IsAny())) .Returns(id => id.Equals(primary.Id) ? primary : id.Equals(staleAlternate.Id) ? staleAlternate : id.Equals(_staleParent.Id) ? _staleParent : null!); StaleAlternateId = staleAlternate.Id; } private Guid StaleAlternateId { get; } public void Dispose() { BaseItem.LibraryManager = _previousLibraryManager!; BaseItem.MediaSourceManager = _previousMediaSourceManager!; BaseItem.ItemRepository = _previousItemRepository!; } [Fact] public void ResolveAlternateVersion_StaleWrongTypeItem_DropsRowWithoutResavingPrimary() { // The alternate is stored under the id of the generic Video type while its primary is a Movie. _libraryManager.ResolveAlternateVersion(AlternatePath, typeof(Movie), null, null); _persistenceServiceMock.Verify( p => p.DeleteItem(It.Is>(ids => ids.Count == 1 && ids[0].Equals(StaleAlternateId))), Times.Once); // Saving the primary is what re-enters this method before the stale row is gone. _persistenceServiceMock.Verify( p => p.SaveItems(It.IsAny>(), It.IsAny()), Times.Never); } [Fact] public void ResolveAlternateVersion_StaleWrongTypeItem_DropsCachedParentListing() { _staleParent.Children = [new Video { Name = "Up - 1080p", Path = AlternatePath }]; _libraryManager.ResolveAlternateVersion(AlternatePath, typeof(Movie), null, null); Assert.Empty(_staleParent.Children); } }