using System; using System.Collections.Generic; using System.Linq; using AutoFixture; using AutoFixture.AutoMoq; using Castle.Components.DictionaryAdapter; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Moq; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library { public class MediaSourceManagerTests { private readonly MediaSourceManager _mediaSourceManager; private readonly Mock _mockUserDataManager; private readonly Mock _mockLocalizationManager; private Video _item; private User _user; public MediaSourceManagerTests() { IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); fixture.Inject(fixture.Create()); _mockUserDataManager = fixture.Freeze>(); _mockUserDataManager.Setup(m => m.GetUserData(It.IsAny(), It.IsAny())).Returns(new UserItemData() { Key = "key" }); _mockLocalizationManager = fixture.Create>(); _mockLocalizationManager.Setup(m => m.FindLanguageInfo(It.IsAny())).Returns((string s) => string.IsNullOrEmpty(s) ? null : new CultureDto(s, s, s, new EditableList { s })); fixture.Inject(_mockLocalizationManager.Object); _mediaSourceManager = fixture.Create(); _item = new Video { Id = Guid.NewGuid(), OwnerId = Guid.Empty, ParentId = Guid.Empty }; _user = fixture.Create(); } [Theory] [InlineData(@"C:\mydir\myfile.ext", MediaProtocol.File)] [InlineData("/mydir/myfile.ext", MediaProtocol.File)] [InlineData("file:///mydir/myfile.ext", MediaProtocol.File)] [InlineData("http://example.com/stream.m3u8", MediaProtocol.Http)] [InlineData("https://example.com/stream.m3u8", MediaProtocol.Http)] [InlineData("rtsp://media.example.com:554/twister/audiotrack", MediaProtocol.Rtsp)] public void GetPathProtocol_ValidArg_Correct(string path, MediaProtocol expected) => Assert.Equal(expected, _mediaSourceManager.GetPathProtocol(path)); [Theory] [InlineData(5, "eng", "eng", false, true)] [InlineData(5, "eng", "eng", true, true)] [InlineData(2, "ger", "eng", false, true)] [InlineData(2, "ger", "eng", true, true)] [InlineData(1, "fre", "eng", false, true)] [InlineData(2, "fre", "eng", true, true)] [InlineData(5, "OriginalLanguage", "eng", false, false)] [InlineData(4, "OriginalLanguage", "eng", false, true)] [InlineData(5, "OriginalLanguage", "eng", true, false)] [InlineData(5, "OriginalLanguage", "eng", true, true)] [InlineData(2, "OriginalLanguage", "jpn", true, true)] [InlineData(2, "OriginalLanguage", "jpn", false, true)] [InlineData(2, "OriginalLanguage", "jpn,eng", false, true)] [InlineData(4, "OriginalLanguage", null, false, true)] [InlineData(2, "OriginalLanguage", null, true, true)] [InlineData(4, "OriginalLanguage", "", false, true)] [InlineData(2, "OriginalLanguage", "", false, false)] [InlineData(2, "OriginalLanguage", "ger", false, true)] [InlineData(2, "OriginalLanguage", "ger", false, false)] [InlineData(1, "OriginalLanguage", "fre", false, false)] [InlineData(2, "OriginalLanguage", "fre", true, true)] [InlineData(2, "OriginalLanguage", "fre", true, false)] public void SetDefaultAudioStreamIndex_Index_Correct( int expectedIndex, string prefferedLanguage, string? originalLanguage, bool playDefault, bool originalExist) { var streams = new MediaStream[] { new() { Index = 0, Type = MediaStreamType.Video, IsDefault = true }, new() { Index = 1, Type = MediaStreamType.Audio, Language = "fre", IsDefault = false, IsOriginal = false }, new() { Index = 2, Type = MediaStreamType.Audio, Language = "jpn", IsDefault = true, IsOriginal = false }, new() { Index = 3, Type = MediaStreamType.Audio, Language = "eng", IsDefault = false, IsOriginal = false }, new() { Index = 4, Type = MediaStreamType.Audio, Language = "eng", IsDefault = false, IsOriginal = originalExist, }, new() { Index = 5, Type = MediaStreamType.Audio, Language = "eng", IsDefault = true, IsOriginal = false, } }; var mediaInfo = new MediaSourceInfo { MediaStreams = streams }; _user.AudioLanguagePreference = prefferedLanguage; _user.PlayDefaultAudioTrack = playDefault; _item.OriginalLanguage = originalLanguage; _mediaSourceManager.SetDefaultAudioAndSubtitleStreamIndices(_item, mediaInfo, _user); Assert.Equal(expectedIndex, mediaInfo.DefaultAudioStreamIndex); } [Fact] public void GetStaticMediaSources_PrimaryQueried_PopulatesPerVersionPositionsAndDefaultsToMostRecent() { var (primary, alt1, alt2) = SetupVersionGroup(); SetupUserDataBatch(new Dictionary { [alt1.Id] = new UserItemData { Key = "alt1", PlaybackPositionTicks = 10, LastPlayedDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, [alt2.Id] = new UserItemData { Key = "alt2", PlaybackPositionTicks = 20, LastPlayedDate = new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc) } }); var sources = _mediaSourceManager.GetStaticMediaSources(primary, false, _user); // Each version carries its own resume point; the primary has none. Assert.Equal((long?)10, sources.First(s => s.Id == alt1.Id.ToString("N")).PlaybackPositionTicks); Assert.Equal((long?)20, sources.First(s => s.Id == alt2.Id.ToString("N")).PlaybackPositionTicks); Assert.Null(sources.First(s => s.Id == primary.Id.ToString("N")).PlaybackPositionTicks); // The most recently played version is the default source, so resuming plays the right file. Assert.Equal(alt2.Id.ToString("N"), sources[0].Id); } [Fact] public void GetStaticMediaSources_AlternateQueried_KeepsOwnSourceFirst() { var (primary, alt1, alt2) = SetupVersionGroup(); SetupUserDataBatch(new Dictionary { [alt2.Id] = new UserItemData { Key = "alt2", PlaybackPositionTicks = 20, LastPlayedDate = new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc) } }); var sources = _mediaSourceManager.GetStaticMediaSources(alt1, false, _user); // An explicitly opened version keeps its own source first, even when a sibling was // played more recently, but the sibling's resume point is still populated. Assert.Equal(alt1.Id.ToString("N"), sources[0].Id); Assert.Equal((long?)20, sources.First(s => s.Id == alt2.Id.ToString("N")).PlaybackPositionTicks); Assert.Equal(3, sources.Count); } [Fact] public void GetStaticMediaSources_NoProgress_KeepsQueriedItemFirst() { var (primary, _, _) = SetupVersionGroup(); SetupUserDataBatch([]); var sources = _mediaSourceManager.GetStaticMediaSources(primary, false, _user); Assert.Equal(primary.Id.ToString("N"), sources[0].Id); Assert.All(sources, s => Assert.Null(s.PlaybackPositionTicks)); } [Fact] public void GetStaticMediaSources_NoUser_DoesNotTouchUserData() { var (primary, _, _) = SetupVersionGroup(); var sources = _mediaSourceManager.GetStaticMediaSources(primary, false); Assert.Equal(primary.Id.ToString("N"), sources[0].Id); _mockUserDataManager.Verify(x => x.GetUserDataBatch(It.IsAny>(), It.IsAny()), Times.Never); } private void SetupUserDataBatch(Dictionary userData) { _mockUserDataManager .Setup(x => x.GetUserDataBatch(It.IsAny>(), It.IsAny())) .Returns((IReadOnlyList items, User _) => items .Where(i => userData.ContainsKey(i.Id)) .ToDictionary(i => i.Id, i => userData[i.Id])); } private static (Video Primary, Video Alt1, Video Alt2) SetupVersionGroup() { var primary = new Video { Id = Guid.NewGuid(), Path = "/Movies/Movie/Movie.mkv" }; var alt1 = new Video { Id = Guid.NewGuid(), Path = "/Movies/Movie/Movie - 1080p.mkv", PrimaryVersionId = primary.Id }; var alt2 = new Video { Id = Guid.NewGuid(), Path = "/Movies/Movie/Movie - 4K.mkv", PrimaryVersionId = primary.Id }; // BaseItem.GetMediaSources runs against the static service locators. var mediaSourceManager = new Mock(); mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny())).Returns(MediaProtocol.File); mediaSourceManager.Setup(x => x.GetMediaStreams(It.IsAny())).Returns(new List()); mediaSourceManager.Setup(x => x.GetMediaAttachments(It.IsAny())).Returns(new List()); var segmentManager = new Mock(); segmentManager.Setup(x => x.IsTypeSupported(It.IsAny())).Returns(false); var libraryManager = new Mock(); libraryManager.Setup(x => x.GetLinkedAlternateVersions(It.IsAny