diff options
| author | Bond-009 <bond.009@outlook.com> | 2026-05-06 20:49:28 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-06 20:49:28 +0200 |
| commit | 2fbb8215818b9cd17f6e6aa8cea3a6961520387d (patch) | |
| tree | ee68da202f604eef267254ea8c689965098b1c3e | |
| parent | d1ab428476f961426841a0561036c59c3b93878e (diff) | |
| parent | 33ed52b8ee25e1fae4763a26337b838dc9782b26 (diff) | |
Merge pull request #16472 from IDisposable/feature/season-provider-id-from-path
Parse provider IDs from season and episode folder/file names
17 files changed, 1258 insertions, 4 deletions
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 5fd23c9f50..85bf20cc2a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -1,8 +1,10 @@ #nullable disable using System; +using System.IO; using System.Linq; using Emby.Naming.Common; +using Emby.Server.Implementations.Library; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; @@ -81,10 +83,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV episode.ParentIndexNumber = 1; } + SetProviderIdFromPath(episode, args.Path); + return episode; } return null; } + + /// <summary> + /// Sets provider ids from the episode file name. + /// </summary> + /// <param name="item">The episode.</param> + /// <param name="path">The episode file path.</param> + private static void SetProviderIdFromPath(Episode item, string path) + { + var justName = Path.GetFileNameWithoutExtension(path.AsSpan()); + + var imdbId = justName.GetAttributeValue("imdbid"); + item.TrySetProviderId(MetadataProvider.Imdb, imdbId); + + var tvdbId = justName.GetAttributeValue("tvdbid"); + item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId); + + var tvmazeId = justName.GetAttributeValue("tvmazeid"); + item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId); + + var tmdbId = justName.GetAttributeValue("tmdbid"); + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); + } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index d4e8e0a63a..6e9a38fd34 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,12 +1,15 @@ #nullable disable +using System; using System.Globalization; using System.IO; using System.Linq; using Emby.Naming.Common; using Emby.Naming.TV; +using Emby.Server.Implementations.Library; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; @@ -101,10 +104,31 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV args.LibraryOptions.PreferredMetadataLanguage); } + SetProviderIdFromPath(season, path); + return season; } return null; } + + /// <summary> + /// Sets provider ids from the season folder name. + /// </summary> + /// <param name="item">The season.</param> + /// <param name="path">The season folder path.</param> + private static void SetProviderIdFromPath(Season item, string path) + { + var justName = Path.GetFileName(path.AsSpan()); + + var tvdbId = justName.GetAttributeValue("tvdbid"); + item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId); + + var tvmazeId = justName.GetAttributeValue("tvmazeid"); + item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId); + + var tmdbId = justName.GetAttributeValue("tmdbid"); + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); + } } } diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs index 980bac102e..67cb85de69 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -17,6 +18,18 @@ public class ImdbExternalUrlProvider : IExternalUrlProvider public IEnumerable<string> GetExternalUrls(BaseItem item) { var baseUrl = "https://www.imdb.com/"; + + if (item is Season season) + { + if (season.Series?.TryGetProviderId(MetadataProvider.Imdb, out var seriesImdbId) == true + && season.IndexNumber.HasValue) + { + yield return baseUrl + $"title/{seriesImdbId}/episodes/?season={season.IndexNumber.Value}"; + } + + yield break; + } + if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId)) { if (item is Person) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeExternalId.cs new file mode 100644 index 0000000000..8d9d2d354b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeExternalId.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.Tmdb.TV +{ + /// <summary> + /// External id for a TMDb episode. + /// </summary> + public class TmdbEpisodeExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProvider.Tmdb.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Episode; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Episode; + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonExternalId.cs new file mode 100644 index 0000000000..8191446363 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonExternalId.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.Tmdb.TV +{ + /// <summary> + /// External id for a TMDb season. + /// </summary> + public class TmdbSeasonExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProvider.Tmdb.ToString(); + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Season; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Season; + } +} diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs index 840cec9841..477bcc6f0c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs @@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public ExternalIdMediaType? Type => ExternalIdMediaType.Series; /// <inheritdoc /> - public bool Supports(IHasProviderIds item) - { - return item is Series; - } + public bool Supports(IHasProviderIds item) => item is Series; } } diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/AudioDbExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/AudioDbExternalUrlProviderTests.cs new file mode 100644 index 0000000000..a9161a0402 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/AudioDbExternalUrlProviderTests.cs @@ -0,0 +1,89 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Plugins.AudioDb; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + public sealed class AudioDbExternalUrlProviderTests + { + private readonly AudioDbAlbumExternalUrlProvider _albumProvider = new(); + private readonly AudioDbArtistExternalUrlProvider _artistProvider = new(); + + [Fact] + public void GetExternalUrls_MusicAlbumWithAudioDbAlbumId_ReturnsCorrectUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.AudioDbAlbum, "12345"); + + var urls = _albumProvider.GetExternalUrls(album); + + Assert.Contains("https://www.theaudiodb.com/album/12345", urls); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithNoAudioDbAlbumId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + + var urls = _albumProvider.GetExternalUrls(album); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonAlbumWithAudioDbAlbumId_ReturnsNoUrl() + { + var artist = new MusicArtist(); + artist.SetProviderId(MetadataProvider.AudioDbAlbum, "12345"); + + var urls = _albumProvider.GetExternalUrls(artist); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_MusicArtistWithAudioDbArtistId_ReturnsCorrectUrl() + { + var artist = new MusicArtist(); + artist.SetProviderId(MetadataProvider.AudioDbArtist, "67890"); + + var urls = _artistProvider.GetExternalUrls(artist); + + Assert.Contains("https://www.theaudiodb.com/artist/67890", urls); + } + + [Fact] + public void GetExternalUrls_PersonWithAudioDbArtistId_ReturnsCorrectUrl() + { + var person = new Person(); + person.SetProviderId(MetadataProvider.AudioDbArtist, "67890"); + + var urls = _artistProvider.GetExternalUrls(person); + + Assert.Contains("https://www.theaudiodb.com/artist/67890", urls); + } + + [Fact] + public void GetExternalUrls_MusicArtistWithNoAudioDbArtistId_ReturnsNoUrl() + { + var artist = new MusicArtist(); + + var urls = _artistProvider.GetExternalUrls(artist); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonArtistWithAudioDbArtistId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.AudioDbArtist, "67890"); + + var urls = _artistProvider.GetExternalUrls(album); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs new file mode 100644 index 0000000000..99604e0933 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs @@ -0,0 +1,56 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Plugins.ComicVine; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + public sealed class ComicVineExternalUrlProviderTests + { + private readonly ComicVineExternalUrlProvider _provider = new(); + + [Fact] + public void GetExternalUrls_PersonWithComicVineId_ReturnsCorrectUrl() + { + var person = new Person(); + person.SetProviderId("ComicVine", "person/4005-1234"); + + var urls = _provider.GetExternalUrls(person); + + Assert.Contains("https://comicvine.gamespot.com/person/4005-1234", urls); + } + + [Fact] + public void GetExternalUrls_BookWithComicVineId_ReturnsCorrectUrl() + { + var book = new Book(); + book.SetProviderId("ComicVine", "issue/4000-5678"); + + var urls = _provider.GetExternalUrls(book); + + Assert.Contains("https://comicvine.gamespot.com/issue/4000-5678", urls); + } + + [Fact] + public void GetExternalUrls_PersonWithNoComicVineId_ReturnsNoUrl() + { + var person = new Person(); + + var urls = _provider.GetExternalUrls(person); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonSupportedItemWithComicVineId_ReturnsNoUrl() + { + var series = new Series(); + series.SetProviderId("ComicVine", "volume/4050-9999"); + + var urls = _provider.GetExternalUrls(series); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs new file mode 100644 index 0000000000..eec64ac53f --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Plugins.GoogleBooks; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + public sealed class GoogleBooksExternalUrlProviderTests + { + private readonly GoogleBooksExternalUrlProvider _provider = new(); + + [Fact] + public void GetExternalUrls_BookWithGoogleBooksId_ReturnsCorrectUrl() + { + var book = new Book(); + book.SetProviderId("GoogleBooks", "buc0AAAAMAAJ"); + + var urls = _provider.GetExternalUrls(book); + + Assert.Contains("https://books.google.com/books?id=buc0AAAAMAAJ", urls); + } + + [Fact] + public void GetExternalUrls_BookWithNoGoogleBooksId_ReturnsNoUrl() + { + var book = new Book(); + + var urls = _provider.GetExternalUrls(book); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonBookWithGoogleBooksId_ReturnsNoUrl() + { + var series = new Series(); + series.SetProviderId("GoogleBooks", "buc0AAAAMAAJ"); + + var urls = _provider.GetExternalUrls(series); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/ImdbExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/ImdbExternalUrlProviderTests.cs new file mode 100644 index 0000000000..ed4a8e7478 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/ImdbExternalUrlProviderTests.cs @@ -0,0 +1,125 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Movies; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + // put tests that mock the static LibraryManager in the same collection to avoid test interference + [Collection("LibraryManagerTests")] + public sealed class ImdbExternalUrlProviderTests : IDisposable + { + private readonly ImdbExternalUrlProvider _provider = new(); + private readonly Mock<ILibraryManager> _libraryManagerMock = new(); + private readonly ILibraryManager? _previousLibraryManager; + + public ImdbExternalUrlProviderTests() + { + _previousLibraryManager = BaseItem.LibraryManager; + BaseItem.LibraryManager = _libraryManagerMock.Object; + } + + public void Dispose() + { + BaseItem.LibraryManager = _previousLibraryManager; + } + + [Fact] + public void GetExternalUrls_MovieWithImdbId_ReturnsCorrectUrl() + { + var movie = new Movie(); + movie.SetProviderId(MetadataProvider.Imdb, "tt1234567"); + + var urls = _provider.GetExternalUrls(movie); + + Assert.Contains("https://www.imdb.com/title/tt1234567", urls); + } + + [Fact] + public void GetExternalUrls_SeriesWithImdbId_ReturnsCorrectUrl() + { + var series = new Series(); + series.SetProviderId(MetadataProvider.Imdb, "tt7654321"); + + var urls = _provider.GetExternalUrls(series); + + Assert.Contains("https://www.imdb.com/title/tt7654321", urls); + } + + [Fact] + public void GetExternalUrls_EpisodeWithImdbId_ReturnsCorrectUrl() + { + var episode = new Episode(); + episode.SetProviderId(MetadataProvider.Imdb, "tt9999999"); + + var urls = _provider.GetExternalUrls(episode); + + Assert.Contains("https://www.imdb.com/title/tt9999999", urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithSeriesImdbId_ReturnsSeasonEpisodesUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + series.SetProviderId(MetadataProvider.Imdb, "tt1234567"); + + var season = new Season { IndexNumber = 2, SeriesId = series.Id }; + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + + var urls = _provider.GetExternalUrls(season); + + Assert.Contains("https://www.imdb.com/title/tt1234567/episodes/?season=2", urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithNoSeriesImdbId_ReturnsNoUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + var season = new Season { IndexNumber = 1, SeriesId = series.Id }; + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + + var urls = _provider.GetExternalUrls(season); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithNoIndexNumber_ReturnsNoUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + series.SetProviderId(MetadataProvider.Imdb, "tt1234567"); + var season = new Season { IndexNumber = null, SeriesId = series.Id }; + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + + var urls = _provider.GetExternalUrls(season); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithUnknownSeriesId_ReturnsNoUrl() + { + var season = new Season { IndexNumber = 1, SeriesId = Guid.NewGuid() }; + _libraryManagerMock.Setup(m => m.GetItemById(It.IsAny<Guid>())).Returns((BaseItem?)null); + + var urls = _provider.GetExternalUrls(season); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_ItemWithNoImdbId_ReturnsNoUrl() + { + var movie = new Movie(); + + var urls = _provider.GetExternalUrls(movie); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/IsbnExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/IsbnExternalUrlProviderTests.cs new file mode 100644 index 0000000000..228a9d2656 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/IsbnExternalUrlProviderTests.cs @@ -0,0 +1,45 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Books.Isbn; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + public sealed class IsbnExternalUrlProviderTests + { + private readonly IsbnExternalUrlProvider _provider = new(); + + [Fact] + public void GetExternalUrls_BookWithIsbnId_ReturnsCorrectUrl() + { + var book = new Book(); + book.SetProviderId("ISBN", "9780306406157"); + + var urls = _provider.GetExternalUrls(book); + + Assert.Contains("https://search.worldcat.org/search?q=bn:9780306406157", urls); + } + + [Fact] + public void GetExternalUrls_BookWithNoIsbnId_ReturnsNoUrl() + { + var book = new Book(); + + var urls = _provider.GetExternalUrls(book); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonBookWithIsbnId_ReturnsNoUrl() + { + var series = new Series(); + series.SetProviderId("ISBN", "9780306406157"); + + var urls = _provider.GetExternalUrls(series); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/MusicBrainzExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/MusicBrainzExternalUrlProviderTests.cs new file mode 100644 index 0000000000..920529bf73 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/MusicBrainzExternalUrlProviderTests.cs @@ -0,0 +1,201 @@ +using System; +using System.Reflection; +using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Plugins.MusicBrainz; +using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + public sealed class MusicBrainzExternalUrlProviderTests : IDisposable + { + private static readonly PropertyInfo _instanceProperty = + typeof(Plugin).GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)!; + + private static readonly MethodInfo _instanceSetter = + _instanceProperty.GetSetMethod(nonPublic: true)!; + + private readonly Plugin? _previousPlugin; + + public MusicBrainzExternalUrlProviderTests() + { + _previousPlugin = Plugin.Instance; + + var appPathsMock = new Mock<IApplicationPaths>(); + appPathsMock.Setup(p => p.PluginsPath).Returns(System.IO.Path.GetTempPath()); + appPathsMock.Setup(p => p.PluginConfigurationsPath).Returns(System.IO.Path.GetTempPath()); + + var xmlSerializerMock = new Mock<IXmlSerializer>(); + xmlSerializerMock + .Setup(s => s.DeserializeFromFile(typeof(PluginConfiguration), It.IsAny<string>())) + .Returns(new PluginConfiguration()); + + var appHostMock = new Mock<IApplicationHost>(); + appHostMock.Setup(h => h.Name).Returns("Jellyfin"); + appHostMock.Setup(h => h.ApplicationVersionString).Returns("1.0.0"); + appHostMock.Setup(h => h.ApplicationUserAgentAddress).Returns("localhost"); + + _ = new Plugin(appPathsMock.Object, xmlSerializerMock.Object, appHostMock.Object); + } + + public void Dispose() + { + _instanceSetter.Invoke(null, new object?[] { _previousPlugin }); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithMusicBrainzAlbumId_ReturnsCorrectUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.MusicBrainzAlbum, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzAlbumExternalUrlProvider().GetExternalUrls(album); + + Assert.Contains(PluginConfiguration.DefaultServer + "/release/a1b2c3d4-e5f6-7890-abcd-ef1234567890", urls); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithNoMusicBrainzAlbumId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + + var urls = new MusicBrainzAlbumExternalUrlProvider().GetExternalUrls(album); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonAlbumWithMusicBrainzAlbumId_ReturnsNoUrl() + { + var artist = new MusicArtist(); + artist.SetProviderId(MetadataProvider.MusicBrainzAlbum, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzAlbumExternalUrlProvider().GetExternalUrls(artist); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithMusicBrainzAlbumArtistId_ReturnsCorrectUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzAlbumArtistExternalUrlProvider().GetExternalUrls(album); + + Assert.Contains(PluginConfiguration.DefaultServer + "/artist/a1b2c3d4-e5f6-7890-abcd-ef1234567890", urls); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithNoMusicBrainzAlbumArtistId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + + var urls = new MusicBrainzAlbumArtistExternalUrlProvider().GetExternalUrls(album); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_MusicArtistWithMusicBrainzArtistId_ReturnsCorrectUrl() + { + var artist = new MusicArtist(); + artist.SetProviderId(MetadataProvider.MusicBrainzArtist, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzArtistExternalUrlProvider().GetExternalUrls(artist); + + Assert.Contains(PluginConfiguration.DefaultServer + "/artist/a1b2c3d4-e5f6-7890-abcd-ef1234567890", urls); + } + + [Fact] + public void GetExternalUrls_PersonWithMusicBrainzArtistId_ReturnsCorrectUrl() + { + var person = new Person(); + person.SetProviderId(MetadataProvider.MusicBrainzArtist, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzArtistExternalUrlProvider().GetExternalUrls(person); + + Assert.Contains(PluginConfiguration.DefaultServer + "/artist/a1b2c3d4-e5f6-7890-abcd-ef1234567890", urls); + } + + [Fact] + public void GetExternalUrls_MusicArtistWithNoMusicBrainzArtistId_ReturnsNoUrl() + { + var artist = new MusicArtist(); + + var urls = new MusicBrainzArtistExternalUrlProvider().GetExternalUrls(artist); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonArtistWithMusicBrainzArtistId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.MusicBrainzArtist, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzArtistExternalUrlProvider().GetExternalUrls(album); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithMusicBrainzReleaseGroupId_ReturnsCorrectUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzReleaseGroupExternalUrlProvider().GetExternalUrls(album); + + Assert.Contains(PluginConfiguration.DefaultServer + "/release-group/a1b2c3d4-e5f6-7890-abcd-ef1234567890", urls); + } + + [Fact] + public void GetExternalUrls_MusicAlbumWithNoMusicBrainzReleaseGroupId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + + var urls = new MusicBrainzReleaseGroupExternalUrlProvider().GetExternalUrls(album); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_AudioWithMusicBrainzTrackId_ReturnsCorrectUrl() + { + var audio = new Audio(); + audio.SetProviderId(MetadataProvider.MusicBrainzTrack, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzTrackExternalUrlProvider().GetExternalUrls(audio); + + Assert.Contains(PluginConfiguration.DefaultServer + "/track/a1b2c3d4-e5f6-7890-abcd-ef1234567890", urls); + } + + [Fact] + public void GetExternalUrls_AudioWithNoMusicBrainzTrackId_ReturnsNoUrl() + { + var audio = new Audio(); + + var urls = new MusicBrainzTrackExternalUrlProvider().GetExternalUrls(audio); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_NonAudioWithMusicBrainzTrackId_ReturnsNoUrl() + { + var album = new MusicAlbum(); + album.SetProviderId(MetadataProvider.MusicBrainzTrack, "a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + + var urls = new MusicBrainzTrackExternalUrlProvider().GetExternalUrls(album); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/TmdbExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/TmdbExternalUrlProviderTests.cs new file mode 100644 index 0000000000..814375a49c --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/TmdbExternalUrlProviderTests.cs @@ -0,0 +1,193 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Plugins.Tmdb; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + // put tests that mock the static LibraryManager in the same collection to avoid test interference + [Collection("LibraryManagerTests")] + public sealed class TmdbExternalUrlProviderTests : IDisposable + { + private readonly TmdbExternalUrlProvider _provider = new(); + private readonly Mock<ILibraryManager> _libraryManagerMock = new(); + private readonly ILibraryManager? _previousLibraryManager; + + public TmdbExternalUrlProviderTests() + { + _previousLibraryManager = BaseItem.LibraryManager; + BaseItem.LibraryManager = _libraryManagerMock.Object; + } + + public void Dispose() + { + BaseItem.LibraryManager = _previousLibraryManager; + } + + [Fact] + public void GetExternalUrls_SeriesWithTmdbId_ReturnsCorrectUrl() + { + var series = new Series(); + series.SetProviderId(MetadataProvider.Tmdb, "1399"); + + var urls = _provider.GetExternalUrls(series); + + Assert.Contains(TmdbUtils.BaseTmdbUrl + "tv/1399", urls); + } + + [Fact] + public void GetExternalUrls_SeriesWithNoTmdbId_ReturnsNoUrl() + { + var series = new Series(); + + var urls = _provider.GetExternalUrls(series); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithSeriesTmdbId_ReturnsCorrectUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + series.SetProviderId(MetadataProvider.Tmdb, "1399"); + + var season = new Season { IndexNumber = 3, SeriesId = series.Id }; + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + + var urls = _provider.GetExternalUrls(season); + + Assert.Contains(TmdbUtils.BaseTmdbUrl + "tv/1399/season/3", urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithNoSeriesTmdbId_ReturnsNoUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + var season = new Season { IndexNumber = 1, SeriesId = series.Id }; + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + + var urls = _provider.GetExternalUrls(season); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_SeasonWithNoIndexNumber_ReturnsNoUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + series.SetProviderId(MetadataProvider.Tmdb, "1399"); + var season = new Season { IndexNumber = null, SeriesId = series.Id }; + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + + var urls = _provider.GetExternalUrls(season); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_EpisodeWithSeriesTmdbId_ReturnsCorrectUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + series.SetProviderId(MetadataProvider.Tmdb, "1399"); + + var season = new Season { Id = Guid.NewGuid(), IndexNumber = 2, SeriesId = series.Id }; + + var episode = new Episode + { + IndexNumber = 5, + SeasonId = season.Id, + SeriesId = series.Id + }; + + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + _libraryManagerMock.Setup(m => m.GetItemById(season.Id)).Returns(season); + + var urls = _provider.GetExternalUrls(episode); + + Assert.Contains(TmdbUtils.BaseTmdbUrl + "tv/1399/season/2/episode/5", urls); + } + + [Fact] + public void GetExternalUrls_EpisodeWithNoSeriesTmdbId_ReturnsNoUrl() + { + var series = new Series { Id = Guid.NewGuid() }; + var season = new Season { Id = Guid.NewGuid(), IndexNumber = 1, SeriesId = series.Id }; + var episode = new Episode { IndexNumber = 1, SeasonId = season.Id, SeriesId = series.Id }; + + _libraryManagerMock.Setup(m => m.GetItemById(series.Id)).Returns(series); + _libraryManagerMock.Setup(m => m.GetItemById(season.Id)).Returns(season); + + var urls = _provider.GetExternalUrls(episode); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_MovieWithTmdbId_ReturnsCorrectUrl() + { + var movie = new Movie(); + movie.SetProviderId(MetadataProvider.Tmdb, "550"); + + var urls = _provider.GetExternalUrls(movie); + + Assert.Contains(TmdbUtils.BaseTmdbUrl + "movie/550", urls); + } + + [Fact] + public void GetExternalUrls_MovieWithNoTmdbId_ReturnsNoUrl() + { + var movie = new Movie(); + + var urls = _provider.GetExternalUrls(movie); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_PersonWithTmdbId_ReturnsCorrectUrl() + { + var person = new Person(); + person.SetProviderId(MetadataProvider.Tmdb, "6384"); + + var urls = _provider.GetExternalUrls(person); + + Assert.Contains(TmdbUtils.BaseTmdbUrl + "person/6384", urls); + } + + [Fact] + public void GetExternalUrls_PersonWithNoTmdbId_ReturnsNoUrl() + { + var person = new Person(); + + var urls = _provider.GetExternalUrls(person); + + Assert.Empty(urls); + } + + [Fact] + public void GetExternalUrls_BoxSetWithTmdbId_ReturnsCorrectUrl() + { + var boxSet = new BoxSet(); + boxSet.SetProviderId(MetadataProvider.Tmdb, "10"); + + var urls = _provider.GetExternalUrls(boxSet); + + Assert.Contains(TmdbUtils.BaseTmdbUrl + "collection/10", urls); + } + + [Fact] + public void GetExternalUrls_BoxSetWithNoTmdbId_ReturnsNoUrl() + { + var boxSet = new BoxSet(); + + var urls = _provider.GetExternalUrls(boxSet); + + Assert.Empty(urls); + } + } +} diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/Zap2ItExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/Zap2ItExternalUrlProviderTests.cs new file mode 100644 index 0000000000..dbe46d8fb1 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/Zap2ItExternalUrlProviderTests.cs @@ -0,0 +1,33 @@ +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.TV; +using Xunit; + +namespace Jellyfin.Providers.Tests.ExternalId +{ + public sealed class Zap2ItExternalUrlProviderTests + { + private readonly Zap2ItExternalUrlProvider _provider = new(); + + [Fact] + public void GetExternalUrls_ItemWithZap2ItId_ReturnsCorrectUrl() + { + var series = new Series(); + series.SetProviderId(MetadataProvider.Zap2It, "EP012345678901"); + + var urls = _provider.GetExternalUrls(series); + + Assert.Contains("http://tvlistings.zap2it.com/overview.html?programSeriesId=EP012345678901", urls); + } + + [Fact] + public void GetExternalUrls_ItemWithNoZap2ItId_ReturnsNoUrl() + { + var series = new Series(); + + var urls = _provider.GetExternalUrls(series); + + Assert.Empty(urls); + } + } +} 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/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)); + } + } +} |
