aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs26
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs24
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeExternalId.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonExternalId.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs5
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/AudioDbExternalUrlProviderTests.cs89
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs56
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs45
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/ImdbExternalUrlProviderTests.cs125
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/IsbnExternalUrlProviderTests.cs45
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/MusicBrainzExternalUrlProviderTests.cs201
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/TmdbExternalUrlProviderTests.cs193
-rw-r--r--tests/Jellyfin.Providers.Tests/ExternalId/Zap2ItExternalUrlProviderTests.cs33
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs88
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs145
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs124
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));
+ }
+ }
+}