diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-05-12 22:50:16 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-05-12 22:50:16 +0200 |
| commit | 8f7c54ee5ef8647bc049499819606ad7946378ec (patch) | |
| tree | 4411b82fd0d0660a426b869a5781782e6dee7500 /tests/Jellyfin.Server.Implementations.Tests/Library | |
| parent | 5e82b61bab8c9461624fd2095fc9ccd11e33ce8d (diff) | |
| parent | e9942c385775f33c70dbb4b910085ae2c563e898 (diff) | |
Merge remote-tracking branch 'upstream/master' into search-rebased
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library')
5 files changed, 865 insertions, 0 deletions
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index a7bbef7ed4..03c0b4af39 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,4 +1,9 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Emby.Server.Implementations.Library; +using MediaBrowser.Model.IO; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; @@ -78,4 +83,391 @@ public class DotIgnoreIgnoreRuleTest // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } + + [Fact] + public void CacheHit_RepeatedCallsDoNotRereadFiles() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var subDir = Path.Combine(tempDir, "subdir"); + Directory.CreateDirectory(subDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(subDir, "test.tmp"), + IsDirectory = false + }; + + // First call - should cache + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Second call - should use cache + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result2); + + // Third call with different file in same directory - should use cache + var fileInfo2 = new FileSystemMetadata + { + FullName = Path.Combine(subDir, "other.tmp"), + IsDirectory = false + }; + var result3 = rule.ShouldIgnore(fileInfo2, null); + Assert.True(result3); + + // Call with file that doesn't match pattern + var fileInfo3 = new FileSystemMetadata + { + FullName = Path.Combine(subDir, "other.txt"), + IsDirectory = false + }; + var result4 = rule.ShouldIgnore(fileInfo3, null); + Assert.False(result4); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void CacheInvalidation_ModifyIgnoreFile_Reparses() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.tmp"), + IsDirectory = false + }; + + // First call - should ignore .tmp files + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Modify the .ignore file to ignore .txt instead + // Wait a bit to ensure the file modification time changes + Thread.Sleep(50); + File.WriteAllText(ignoreFilePath, "*.txt"); + + // Now .tmp files should NOT be ignored + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.False(result2); + + // And .txt files SHOULD be ignored + var txtFileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.txt"), + IsDirectory = false + }; + var result3 = rule.ShouldIgnore(txtFileInfo, null); + Assert.True(result3); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void EmptyIgnoreFile_IgnoresEverything() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, string.Empty); + + var rule = new DotIgnoreIgnoreRule(); + + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "anyfile.mkv"), + IsDirectory = false + }; + + // Empty .ignore file should ignore everything + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void WhitespaceOnlyIgnoreFile_IgnoresEverything() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, " \n\t\n "); + + var rule = new DotIgnoreIgnoreRule(); + + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "anyfile.mkv"), + IsDirectory = false + }; + + // Whitespace-only .ignore file should ignore everything + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void NoIgnoreFile_DoesNotIgnore() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var rule = new DotIgnoreIgnoreRule(); + + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "anyfile.mkv"), + IsDirectory = false + }; + + // No .ignore file means don't ignore + var result = rule.ShouldIgnore(fileInfo, null); + Assert.False(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void ConcurrentAccess_ThreadSafe() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + + // Run multiple parallel checks + Parallel.For(0, 100, i => + { + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, $"test{i}.tmp"), + IsDirectory = false + }; + + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + }); + + // Also test with non-matching files + Parallel.For(0, 100, i => + { + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, $"test{i}.txt"), + IsDirectory = false + }; + + var result = rule.ShouldIgnore(fileInfo, null); + Assert.False(result); + }); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void ClearCache_ClearsAllCachedData() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.tmp"), + IsDirectory = false + }; + + // First call to populate cache + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Clear cache + rule.ClearDirectoryCache(); + + // Should still work (will re-populate cache) + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result2); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void IgnoreFileDeleted_HandlesGracefully() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.tmp"), + IsDirectory = false + }; + + // First call - should ignore + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Delete the .ignore file + File.Delete(ignoreFilePath); + + // Should not ignore anymore (file deleted) + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.False(result2); + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + + [Fact] + public void ParentDirectoryIgnoreFile_AppliesToSubdirectories() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var subDir1 = Path.Combine(tempDir, "sub1"); + var subDir2 = Path.Combine(tempDir, "sub1", "sub2"); + Directory.CreateDirectory(subDir1); + Directory.CreateDirectory(subDir2); + + try + { + // Put .ignore in root + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + + // Check file in sub2 - should find .ignore in parent + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(subDir2, "test.tmp"), + IsDirectory = false + }; + + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + + // Check file in sub1 + var fileInfo2 = new FileSystemMetadata + { + FullName = Path.Combine(subDir1, "test.tmp"), + IsDirectory = false + }; + + var result2 = rule.ShouldIgnore(fileInfo2, null); + Assert.True(result2); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void DirectoryMatching_TrailingSlashPattern() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var subDir = Path.Combine(tempDir, "videos"); + Directory.CreateDirectory(subDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "videos/"); + + var rule = new DotIgnoreIgnoreRule(); + + // Directory should be ignored + var dirInfo = new FileSystemMetadata + { + FullName = subDir, + IsDirectory = true + }; + + var result = rule.ShouldIgnore(dirInfo, null); + Assert.True(result); + + // File named "videos" should NOT be ignored (pattern has trailing slash) + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "videos"), + IsDirectory = false + }; + + // Note: The Ignore library behavior may vary here, this tests the actual behavior + var resultFile = rule.ShouldIgnore(fileInfo, null); + // The file named "videos" without trailing slash might or might not match depending on the library + // This test documents the actual behavior + } + finally + { + Directory.Delete(tempDir, true); + } + } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index cc2e47c33a..16b601dc3c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -61,6 +61,94 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.NotNull(episodeResolver.Resolve(itemResolveArgs)); } + [Theory] + [InlineData("/media/Show/Season 01/Show S01E01 [tvdbid=12345].mkv", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01/Show S01E01 [tvdbid-12345].mkv", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01/Show S01E01 (tvdbid=12345).mkv", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 02/Show S02E03 [tvmazeid=67890].mkv", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 02/Show S02E03 [tvmazeid-67890].mkv", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 03/Show S03E04 [tmdbid=99999].mkv", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show/Season 03/Show S03E04 [tmdbid-99999].mkv", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show/Season 04/Show S04E05 [imdbid=tt1234567].mkv", MetadataProvider.Imdb, "tt1234567")] + [InlineData("/media/Show/Season 04/Show S04E05 [imdbid-tt1234567].mkv", MetadataProvider.Imdb, "tt1234567")] + public void Resolve_EpisodeFileWithProviderId_SetsProviderId(string path, MetadataProvider provider, string expectedId) + { + var series = new Series { Name = "Show" }; + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + CollectionType = CollectionType.tvshows, + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = false + } + }; + + var episode = episodeResolver.Resolve(itemResolveArgs); + + Assert.NotNull(episode); + Assert.True(episode.TryGetProviderId(provider, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Fact] + public void Resolve_EpisodeFileWithProviderIdsOnAllLevels_OnlyUsesEpisodeLevelId() + { + // Series folder has tvdbid=11111, season folder has tvdbid=22222, episode file has tvdbid=33333. + // The episode should only pick up its own ID, not the series- or season-level ones. + var series = new Series { Name = "Show" }; + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + CollectionType = CollectionType.tvshows, + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show [tvdbid=11111]/Season 01 [tvdbid=22222]/Show S01E01 [tvdbid=33333].mkv", + IsDirectory = false + } + }; + + var episode = episodeResolver.Resolve(itemResolveArgs); + + Assert.NotNull(episode); + Assert.True(episode.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("33333", tvdbId); + } + + [Fact] + public void Resolve_EpisodeFileWithMultipleProviderIds_SetsAll() + { + var series = new Series { Name = "Show" }; + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + CollectionType = CollectionType.tvshows, + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show/Season 01/Show S01E01 [tvdbid=12345][tmdbid=99999].mkv", + IsDirectory = false + } + }; + + var episode = episodeResolver.Resolve(itemResolveArgs); + + Assert.NotNull(episode); + Assert.True(episode.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("12345", tvdbId); + Assert.True(episode.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)); + Assert.Equal("99999", tmdbId); + } + private sealed class EpisodeResolverMock : EpisodeResolver { public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs index 8ed3d8b944..facdb2bc2e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs @@ -1,9 +1,18 @@ +using System; 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.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 @@ -11,12 +20,28 @@ namespace Jellyfin.Server.Implementations.Tests.Library public class MediaSourceManagerTests { private readonly MediaSourceManager _mediaSourceManager; + private readonly Mock<IUserDataManager> _mockUserDataManager; + private readonly Mock<ILocalizationManager> _mockLocalizationManager; + private Video _item; + private User _user; public MediaSourceManagerTests() { IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); fixture.Inject<IFileSystem>(fixture.Create<ManagedFileSystem>()); + + _mockUserDataManager = fixture.Freeze<Mock<IUserDataManager>>(); + _mockUserDataManager.Setup(m => m.GetUserData(It.IsAny<User>(), It.IsAny<BaseItem>())).Returns(new UserItemData() { Key = "key" }); + + _mockLocalizationManager = fixture.Create<Mock<ILocalizationManager>>(); + _mockLocalizationManager.Setup(m => m.FindLanguageInfo(It.IsAny<string>())).Returns((string s) => string.IsNullOrEmpty(s) ? null : new CultureDto(s, s, s, new EditableList<string> { s })); + fixture.Inject(_mockLocalizationManager.Object); + _mediaSourceManager = fixture.Create<MediaSourceManager>(); + + _item = new Video { Id = Guid.NewGuid(), OwnerId = Guid.Empty, ParentId = Guid.Empty }; + + _user = fixture.Create<User>(); } [Theory] @@ -28,5 +53,96 @@ namespace Jellyfin.Server.Implementations.Tests.Library [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); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs new file mode 100644 index 0000000000..133a3f7d47 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs @@ -0,0 +1,145 @@ +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.TV; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class SeasonResolverTests + { + private static readonly NamingOptions _namingOptions = new(); + private readonly SeasonResolver _resolver; + + public SeasonResolverTests() + { + var localizationMock = new Mock<ILocalizationManager>(); + localizationMock + .Setup(l => l.GetLocalizedString(It.IsAny<string>())) + .Returns("Season {0}"); + + _resolver = new SeasonResolver( + _namingOptions, + localizationMock.Object, + Mock.Of<ILogger<SeasonResolver>>()); + } + + [Theory] + [InlineData("/media/Show/Season 01 [tvdbid=12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01 [tvdbid-12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01 (tvdbid=12345)", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 02 [tvmazeid=67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 02 [tvmazeid-67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 03 [tmdbid=99999]", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show/Season 03 [tmdbid-99999]", MetadataProvider.Tmdb, "99999")] + public void Resolve_SeasonFolderWithProviderId_SetsProviderId(string path, MetadataProvider provider, string expectedId) + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(provider, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Fact] + public void Resolve_SeasonFolderWithMultipleProviderIds_SetsAll() + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show/Season 01 [tvdbid=12345][tmdbid=99999]", + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("12345", tvdbId); + Assert.True(season.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)); + Assert.Equal("99999", tmdbId); + } + + [Fact] + public void Resolve_SeasonFolderWithSeriesProviderIdInParentPath_DoesNotInheritSeriesId() + { + // Series folder has tvdbid=11111, season folder has tvdbid=22222. + // The season should only pick up its own ID, not the series-level one. + var series = new Series { Path = "/media/Show [tvdbid=11111]" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show [tvdbid=11111]/Season 01 [tvdbid=22222]", + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("22222", tvdbId); + } + + [Fact] + public void Resolve_SeasonFolderWithNoProviderId_HasNoProviderIds() + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show/Season 01", + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.False(season.TryGetProviderId(MetadataProvider.Tvdb, out _)); + Assert.False(season.TryGetProviderId(MetadataProvider.TvMaze, out _)); + Assert.False(season.TryGetProviderId(MetadataProvider.Tmdb, out _)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs new file mode 100644 index 0000000000..8dbd5f5b41 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs @@ -0,0 +1,124 @@ +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.TV; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class SeriesResolverTests + { + private static readonly NamingOptions _namingOptions = new(); + private readonly SeriesResolver _resolver; + private readonly Mock<ILibraryManager> _libraryManagerMock; + + public SeriesResolverTests() + { + _libraryManagerMock = new Mock<ILibraryManager>(); + // Return null so that configuredContentType != CollectionType.tvshows, allowing series resolution. + _libraryManagerMock + .Setup(m => m.GetConfiguredContentType(It.IsAny<string>())) + .Returns((CollectionType?)null); + + _resolver = new SeriesResolver(Mock.Of<ILogger<SeriesResolver>>(), _namingOptions); + } + + private MediaBrowser.Controller.Library.ItemResolveArgs MakeTvArgs(string path) => + new(Mock.Of<IServerApplicationPaths>(), _libraryManagerMock.Object) + { + CollectionType = CollectionType.tvshows, + FileSystemChildren = [], + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = true + } + }; + + [Theory] + [InlineData("/media/Show [tvdbid=12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show [tvdbid-12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show (tvdbid=12345)", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show [tvmazeid=67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show [tvmazeid-67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show [tmdbid=99999]", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show [tmdbid-99999]", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show [imdbid=tt1234567]", MetadataProvider.Imdb, "tt1234567")] + [InlineData("/media/Show [imdbid-tt1234567]", MetadataProvider.Imdb, "tt1234567")] + public void ResolvePath_SeriesFolderWithProviderId_SetsProviderId(string path, MetadataProvider provider, string expectedId) + { + var series = _resolver.ResolvePath(MakeTvArgs(path)) as Series; + + Assert.NotNull(series); + Assert.True(series.TryGetProviderId(provider, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Theory] + [InlineData("/media/Show [anidbid=11111]", "AniDB", "11111")] + [InlineData("/media/Show [anilistid=22222]", "AniList", "22222")] + [InlineData("/media/Show [anisearchid=33333]", "AniSearch", "33333")] + public void ResolvePath_SeriesFolderWithAniProviderId_SetsProviderId(string path, string providerKey, string expectedId) + { + var series = _resolver.ResolvePath(MakeTvArgs(path)) as Series; + + Assert.NotNull(series); + Assert.True(series.TryGetProviderId(providerKey, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Fact] + public void ResolvePath_SeriesFolderWithMultipleProviderIds_SetsAll() + { + var series = _resolver.ResolvePath(MakeTvArgs("/media/Show [tvdbid=12345][tmdbid=99999]")) as Series; + + Assert.NotNull(series); + Assert.True(series.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("12345", tvdbId); + Assert.True(series.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)); + Assert.Equal("99999", tmdbId); + } + + [Fact] + public void ResolvePath_SeriesFolderWithNoProviderId_HasNoProviderIds() + { + var series = _resolver.ResolvePath(MakeTvArgs("/media/Show")) as Series; + + Assert.NotNull(series); + Assert.False(series.TryGetProviderId(MetadataProvider.Tvdb, out _)); + Assert.False(series.TryGetProviderId(MetadataProvider.TvMaze, out _)); + Assert.False(series.TryGetProviderId(MetadataProvider.Tmdb, out _)); + Assert.False(series.TryGetProviderId(MetadataProvider.Imdb, out _)); + Assert.False(series.TryGetProviderId("AniDB", out _)); + Assert.False(series.TryGetProviderId("AniList", out _)); + Assert.False(series.TryGetProviderId("AniSearch", out _)); + } + + [Fact] + public void ResolvePath_SeriesFolderNotInTvShowsCollection_DoesNotResolve() + { + // Without CollectionType.tvshows, a plain folder with no tvshow.nfo and + // no season/episode children should not resolve as a Series. + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + _libraryManagerMock.Object) + { + CollectionType = null, + FileSystemChildren = [], + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show [tvdbid=12345]", + IsDirectory = true + } + }; + + Assert.Null(_resolver.ResolvePath(args)); + } + } +} |
