aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Server.Implementations.Tests/Library
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Jellyfin.Server.Implementations.Tests/Library')
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs88
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs220
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs59
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs145
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs124
5 files changed, 635 insertions, 1 deletions
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..c80f899498 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs
@@ -1,9 +1,22 @@
+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
@@ -11,12 +24,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 +57,196 @@ 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);
+ }
+
+ [Fact]
+ public void GetStaticMediaSources_PrimaryQueried_DefaultsToMostRecentlyPlayedVersion()
+ {
+ var (primary, alt1, alt2) = SetupVersionGroup();
+ SetupUserDataBatch(new Dictionary<Guid, UserItemData>
+ {
+ [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);
+
+ // The most recently played version is the default source, so resuming plays the right file.
+ // Per-user positions live in each version's UserData, not on the source.
+ Assert.Equal(alt2.Id.ToString("N"), sources[0].Id);
+ }
+
+ [Fact]
+ public void GetStaticMediaSources_AlternateQueried_KeepsOwnSourceFirst()
+ {
+ var (primary, alt1, alt2) = SetupVersionGroup();
+ SetupUserDataBatch(new Dictionary<Guid, UserItemData>
+ {
+ [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.
+ Assert.Equal(alt1.Id.ToString("N"), sources[0].Id);
+ 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);
+ }
+
+ [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<IReadOnlyList<BaseItem>>(), It.IsAny<User>()), Times.Never);
+ }
+
+ private void SetupUserDataBatch(Dictionary<Guid, UserItemData> userData)
+ {
+ _mockUserDataManager
+ .Setup(x => x.GetUserDataBatch(It.IsAny<IReadOnlyList<BaseItem>>(), It.IsAny<User>()))
+ .Returns((IReadOnlyList<BaseItem> 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<IMediaSourceManager>();
+ mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>())).Returns(MediaProtocol.File);
+ mediaSourceManager.Setup(x => x.GetMediaStreams(It.IsAny<Guid>())).Returns(new List<MediaStream>());
+ mediaSourceManager.Setup(x => x.GetMediaAttachments(It.IsAny<Guid>())).Returns(new List<MediaAttachment>());
+
+ var segmentManager = new Mock<IMediaSegmentManager>();
+ segmentManager.Setup(x => x.IsTypeSupported(It.IsAny<BaseItem>())).Returns(false);
+
+ var libraryManager = new Mock<ILibraryManager>();
+ libraryManager.Setup(x => x.GetLinkedAlternateVersions(It.IsAny<Video>())).Returns(Array.Empty<Video>());
+ libraryManager.Setup(x => x.GetLocalAlternateVersionIds(primary)).Returns(new[] { alt1.Id, alt2.Id });
+ libraryManager.Setup(x => x.GetLocalAlternateVersionIds(alt1)).Returns(Array.Empty<Guid>());
+ libraryManager.Setup(x => x.GetLocalAlternateVersionIds(alt2)).Returns(Array.Empty<Guid>());
+ libraryManager.Setup(x => x.GetItemById(primary.Id)).Returns(primary);
+ libraryManager.Setup(x => x.GetItemById(alt1.Id)).Returns(alt1);
+ libraryManager.Setup(x => x.GetItemById(alt2.Id)).Returns(alt2);
+
+ var recordingsManager = new Mock<IRecordingsManager>();
+ recordingsManager.Setup(x => x.GetActiveRecordingInfo(It.IsAny<string>())).Returns((ActiveRecordingInfo?)null);
+
+ BaseItem.MediaSegmentManager = segmentManager.Object;
+ BaseItem.MediaSourceManager = mediaSourceManager.Object;
+ BaseItem.LibraryManager = libraryManager.Object;
+ Video.RecordingsManager = recordingsManager.Object;
+
+ return (primary, alt1, alt2);
+ }
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
index aed584355c..e1346a8436 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
@@ -1,7 +1,13 @@
+using System.Collections.Generic;
using Emby.Naming.Common;
+using Emby.Naming.Video;
using Emby.Server.Implementations.Library.Resolvers.Movies;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
@@ -14,11 +20,12 @@ namespace Jellyfin.Server.Implementations.Tests.Library;
public class MovieResolverTests
{
private static readonly NamingOptions _namingOptions = new();
+ private static readonly VideoListResolver _videoListResolver = new(_namingOptions);
[Fact]
public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
{
- var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>());
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
var itemResolveArgs = new ItemResolveArgs(
Mock.Of<IServerApplicationPaths>(),
null)
@@ -32,4 +39,54 @@ public class MovieResolverTests
Assert.NotNull(movieResolver.Resolve(itemResolveArgs));
}
+
+ [Fact]
+ public void ResolveMultiple_GivenTvShowsCollection_CreatesEpisodeItems()
+ {
+ // For a tvshows collection, the multi-version grouping must still produce
+ // Episode BaseItems (not generic Video) so downstream metadata fetching
+ // and series-aware logic apply.
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
+
+ var parent = new Folder { Path = "/TV/Show/Season 1" };
+ var files = new List<FileSystemMetadata>
+ {
+ new() { FullName = "/TV/Show/Season 1/Show - S01E01 - 1080p.mkv", Name = "Show - S01E01 - 1080p.mkv", IsDirectory = false },
+ new() { FullName = "/TV/Show/Season 1/Show - S01E01 - 720p.mkv", Name = "Show - S01E01 - 720p.mkv", IsDirectory = false },
+ new() { FullName = "/TV/Show/Season 1/Show - S01E02.mkv", Name = "Show - S01E02.mkv", IsDirectory = false }
+ };
+
+ var result = movieResolver.ResolveMultiple(parent, files, CollectionType.tvshows, Mock.Of<IDirectoryService>());
+
+ Assert.NotNull(result);
+ Assert.Equal(2, result.Items.Count);
+ Assert.All(result.Items, item => Assert.IsType<Episode>(item));
+
+ // The S01E01 item should have one alternate version
+ var s01e01 = result.Items.Find(i => i.Path.Contains("S01E01", System.StringComparison.Ordinal));
+ Assert.NotNull(s01e01);
+ Assert.Single(((Video)s01e01).LocalAlternateVersions);
+ }
+
+ [Fact]
+ public void ResolveMultiple_GivenMoviesCollection_CreatesMovieItems()
+ {
+ // For a movies collection, the multi-version grouping must produce Movie
+ // BaseItems (not generic Video) so downstream movie-specific logic applies.
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), Mock.Of<ILogger<MovieResolver>>(), _namingOptions, Mock.Of<IDirectoryService>(), _videoListResolver);
+
+ var parent = new Folder { Path = "/movies/Inception (2010)" };
+ var files = new List<FileSystemMetadata>
+ {
+ new() { FullName = "/movies/Inception (2010)/Inception (2010) - 1080p.mkv", Name = "Inception (2010) - 1080p.mkv", IsDirectory = false },
+ new() { FullName = "/movies/Inception (2010)/Inception (2010) - 720p.mkv", Name = "Inception (2010) - 720p.mkv", IsDirectory = false }
+ };
+
+ var result = movieResolver.ResolveMultiple(parent, files, CollectionType.movies, Mock.Of<IDirectoryService>());
+
+ Assert.NotNull(result);
+ Assert.Single(result.Items);
+ Assert.All(result.Items, item => Assert.IsType<Movie>(item));
+ Assert.Single(((Video)result.Items[0]).LocalAlternateVersions);
+ }
}
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));
+ }
+ }
+}