diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-05-12 22:50:16 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-05-12 22:50:16 +0200 |
| commit | 8f7c54ee5ef8647bc049499819606ad7946378ec (patch) | |
| tree | 4411b82fd0d0660a426b869a5781782e6dee7500 /tests | |
| parent | 5e82b61bab8c9461624fd2095fc9ccd11e33ce8d (diff) | |
| parent | e9942c385775f33c70dbb4b910085ae2c563e898 (diff) | |
Merge remote-tracking branch 'upstream/master' into search-rebased
Diffstat (limited to 'tests')
31 files changed, 2160 insertions, 17 deletions
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs index fc969527e8..1406c8ee91 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs @@ -15,10 +15,17 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist } [Fact] - public void ComputeSegments_InvalidDuration_ThrowsArgumentException() + public void ComputeSegments_ZeroDurationOvershoot_ClampsToDuration() { var keyframeData = new KeyframeData(0, new[] { MsToTicks(10000) }); - Assert.Throws<ArgumentException>(() => DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000)); + Assert.Equal(new[] { 10.0 }, DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000)); + } + + [Fact] + public void ComputeSegments_MinorDurationOvershoot_ClampsToDuration() + { + var keyframeData = new KeyframeData(MsToTicks(9900), new[] { 0L, MsToTicks(5000), MsToTicks(10000) }); + Assert.Equal(new[] { 10.0 }, DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000)); } [Theory] diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 3369af0e84..198cdaa4fc 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -105,10 +105,12 @@ namespace Jellyfin.MediaEncoding.Tests.Probing var audio1 = res.MediaStreams[1]; Assert.Equal("eac3", audio1.Codec); + Assert.True(audio1.IsOriginal); Assert.Equal(AudioSpatialFormat.DolbyAtmos, audio1.AudioSpatialFormat); var audio2 = res.MediaStreams[2]; Assert.Equal("dts", audio2.Codec); + Assert.False(audio2.IsOriginal); Assert.Equal(AudioSpatialFormat.DTSX, audio2.AudioSpatialFormat); Assert.Empty(res.Chapters); @@ -156,6 +158,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal("aac", res.MediaStreams[1].Codec); Assert.Equal(7, res.MediaStreams[1].Channels); Assert.True(res.MediaStreams[1].IsDefault); + Assert.False(res.MediaStreams[1].IsOriginal); Assert.Equal("eng", res.MediaStreams[1].Language); Assert.Equal("Surround 6.1", res.MediaStreams[1].Title); diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 8269ae58cd..0b103debad 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -171,6 +171,9 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "Transcode")] // Full transcode because profile only has ts which does not allow vp9 + [InlineData("AndroidTVExoPlayer", "mp4-hevc-aac-4000k-r180", PlayMethod.DirectPlay)] // #13712 + // AndroidTV NoHevcRotation + [InlineData("AndroidTVExoPlayer-NoHevcRotation", "mp4-hevc-aac-4000k-r180", PlayMethod.Transcode, TranscodeReason.VideoRotationNotSupported, "Transcode")] // #13712 // Tizen 3 Stereo [InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json new file mode 100644 index 0000000000..341638bc52 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer-NoHevcRotation.json @@ -0,0 +1,162 @@ +{ + "Name": "Jellyfin AndroidTV-ExoPlayer", + "EnableAlbumArtInDidl": false, + "EnableSingleAlbumArtLimit": false, + "EnableSingleSubtitleLimit": false, + "SupportedMediaTypes": "Audio,Photo,Video", + "MaxAlbumArtWidth": 0, + "MaxAlbumArtHeight": 0, + "MaxStreamingBitrate": 120000000, + "MaxStaticBitrate": 100000000, + "MusicStreamingTranscodingBitrate": 192000, + "TimelineOffsetSeconds": 0, + "RequiresPlainVideoItems": false, + "RequiresPlainFolders": false, + "EnableMSMediaReceiverRegistrar": false, + "IgnoreTranscodeByteRangeRequests": false, + "DirectPlayProfiles": [ + { + "Container": "m4v,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,mp4,webm", + "AudioCodec": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw", + "VideoCodec": "h264,hevc,vp8,vp9,mpeg,mpeg2video", + "Type": "Video", + "$type": "DirectPlayProfile" + }, + { + "Container": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw,,pa,flac,wav,wma,ogg,oga,webma,ape,opus", + "Type": "Audio", + "$type": "DirectPlayProfile" + }, + { + "Container": "jpg,jpeg,png,gif,web", + "Type": "Photo", + "$type": "DirectPlayProfile" + } + ], + "CodecProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "EqualsAny", + "Property": "VideoProfile", + "Value": "main|main 10", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "LessThanEqual", + "Property": "VideoLevel", + "Value": "51", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "Equals", + "Property": "VideoRotation", + "Value": "0", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "Codec": "hevc", + "$type": "CodecProfile" + } + ], + "TranscodingProfiles": [ + { + "Container": "ts", + "Type": "Video", + "VideoCodec": "h264", + "AudioCodec": "aac,mp3", + "Protocol": "hls", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "$type": "TranscodingProfile" + }, + { + "Container": "mp3", + "Type": "Audio", + "AudioCodec": "mp3", + "Protocol": "http", + "EstimateContentLength": false, + "EnableMpegtsM2TsMode": false, + "TranscodeSeekInfo": "Auto", + "CopyTimestamps": false, + "Context": "Streaming", + "EnableSubtitlesInManifest": false, + "MinSegments": 0, + "SegmentLength": 0, + "$type": "TranscodingProfile" + } + ], + "SubtitleProfiles": [ + { + "Format": "srt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "srt", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "subrip", + "Method": "External", + "$type": "SubtitleProfile" + }, + { + "Format": "ass", + "Method": "Encode", + "$type": "SubtitleProfile" + }, + { + "Format": "ssa", + "Method": "Encode", + "$type": "SubtitleProfile" + }, + { + "Format": "pgs", + "Method": "Encode", + "$type": "SubtitleProfile" + }, + { + "Format": "pgssub", + "Method": "Encode", + "$type": "SubtitleProfile" + }, + { + "Format": "dvdsub", + "Method": "Encode", + "$type": "SubtitleProfile" + }, + { + "Format": "vtt", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "sub", + "Method": "Embed", + "$type": "SubtitleProfile" + }, + { + "Format": "idx", + "Method": "Embed", + "$type": "SubtitleProfile" + } + ], + "$type": "DeviceProfile" +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json new file mode 100644 index 0000000000..393b10171d --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-hevc-aac-4000k-r180.json @@ -0,0 +1,56 @@ +{ + "Id": "b7a9e2d4c815f36b0d9241a7e58c3f42", + "Path": "/Media/MyVideo-1080p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 1421636271, + "Name": "MyVideo-1080p", + "ETag": "d8e2a1b5c4f907e8a1d2b3c4e5f6a7b8", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "hevc", + "CodecTag": "hvc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "1080p HEVC SDR", + "NalLengthSize": "0", + "BitRate": 4014613, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 1080, + "Width": 1920, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "Main", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 50, + "Rotation": 180 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 125427, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + } + ], + "Bitrate": 4331578, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs index 7bfab570b7..abdade8f6d 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs @@ -80,7 +80,9 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("[VCB-Studio] Re Zero kara Hajimeru Isekai Seikatsu [21][Ma10p_1080p][x265_flac].mkv", 21)] [InlineData("[CASO&Sumisora][Oda_Nobuna_no_Yabou][04][BDRIP][1920x1080][x264_AAC][7620E503].mp4", 4)] - // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number + [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number + [InlineData("Season 2/Hunter X Hunter - 101.mkv", 101)] // triple digit episode number without brackets + [InlineData("Season 1/Show Name - 1234 [720p].mkv", 1234)] // four digit episode number with quality tag // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)] // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)] // TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)] diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs index 7f2188a3eb..9fef6c517a 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberWithoutSeasonTests.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Naming.Tests.TV [InlineData(2, "The Simpsons/The Simpsons - 02.avi")] [InlineData(2, "The Simpsons/The Simpsons - 02 Ep Name.avi")] [InlineData(7, "GJ Club (2013)/GJ Club - 07.mkv")] - [InlineData(17, "Case Closed (1996-2007)/Case Closed - 317.mkv")] + [InlineData(317, "Case Closed (1996-2007)/Case Closed - 317.mkv")] // TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")] // TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")] // TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")] diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs index ab825c9fa7..09bae2adab 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonNumberTests.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)] [InlineData("Series/1-12 - The Woman.mp4", 1)] [InlineData("Running Man/Running Man S2017E368.mkv", 2017)] - [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 3)] + [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", null)] // TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)] public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected) { diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index b63009d6a5..66eec077dc 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -7,6 +7,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -94,10 +95,47 @@ namespace Jellyfin.Networking.Tests [InlineData("256.128.0.0.0.1")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")] + [InlineData("fd23:184f:2029:0100/56")] public static void TryParseInvalidIPStringsFalse(string address) => Assert.False(NetworkUtils.TryParseToSubnet(address, out _)); /// <summary> + /// Verifies that <see cref="NetworkUtils.TryParseToSubnets"/> emits a targeted warning + /// for IPv6 prefix-only notation and a generic warning for other malformed entries. + /// </summary> + [Fact] + public static void TryParseToSubnets_InvalidEntries_LogsWarnings() + { + var logger = new Mock<ILogger>(); + + var values = new[] { "10.0.0.0/8", "fd23:184f:2029:0100/56", "not-an-address" }; + Assert.True(NetworkUtils.TryParseToSubnets(values, out var result, false, logger.Object)); + Assert.NotNull(result); + Assert.Single(result); + + // IPv6 prefix-only notation should produce a specific, actionable warning. + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny<EventId>(), + It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("IPv6 prefix-only", StringComparison.Ordinal) + && state.ToString()!.Contains("fd23:184f:2029:0100/56", StringComparison.Ordinal)), + It.IsAny<Exception>(), + It.IsAny<Func<It.IsAnyType, Exception?, string>>()), + Times.Once); + + // Other malformed entries should still produce a generic warning. + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny<EventId>(), + It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("not-an-address", StringComparison.Ordinal)), + It.IsAny<Exception>(), + It.IsAny<Func<It.IsAnyType, Exception?, string>>()), + Times.Once); + } + + /// <summary> /// Checks if IPv4 address is within a defined subnet. /// </summary> /// <param name="netMask">Network mask.</param> 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..d35211f387 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/ExternalId/MusicBrainzExternalUrlProviderTests.cs @@ -0,0 +1,202 @@ +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 Microsoft.Extensions.Logging.Abstractions; +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, NullLogger<Plugin>.Instance); + } + + 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.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs index 76922af8d5..a7491f42e9 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs @@ -45,8 +45,9 @@ public class FFProbeVideoInfoTests [Theory] [InlineData(null, 0)] [InlineData(0L, 0)] - [InlineData(1L, 0)] - [InlineData(TimeSpan.TicksPerMinute * 5, 0)] + [InlineData(1L, 1)] + [InlineData(TimeSpan.TicksPerMinute * 3, 1)] + [InlineData(TimeSpan.TicksPerMinute * 5, 1)] [InlineData((TimeSpan.TicksPerMinute * 5) + 1, 1)] [InlineData(TimeSpan.TicksPerMinute * 50, 10)] public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount) @@ -58,4 +59,20 @@ public class FFProbeVideoInfoTests Assert.Equal(chaptersCount, chapters.Length); } + + [Theory] + [InlineData(1L)] + [InlineData(TimeSpan.TicksPerMinute * 3)] + [InlineData(TimeSpan.TicksPerMinute * 5)] + [InlineData((TimeSpan.TicksPerMinute * 5) + 1)] + [InlineData((TimeSpan.TicksPerMinute * 50) + 1)] + public void CreateDummyChapters_PositiveRuntime_NoChapterBeyondRuntime(long runtime) + { + var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() + { + RunTimeTicks = runtime + }); + + Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime)); + } } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs index 222e624aa2..876f18741f 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs @@ -123,13 +123,13 @@ public class MediaInfoResolverTests var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); // any path other than test target exists and provides an empty listing - directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>())) .Returns(Array.Empty<string>()); _subtitleResolver.GetExternalFiles(video.Object, directoryService.Object, false); directoryService.Verify( - ds => ds.GetFilePaths(It.IsRegex(pathNotFoundRegex), It.IsAny<bool>(), It.IsAny<bool>()), + ds => ds.GetFilePaths(It.IsRegex(pathNotFoundRegex), It.IsAny<bool>()), Times.Never); } @@ -196,7 +196,7 @@ public class MediaInfoResolverTests }; var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); - directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>())) .Returns(Array.Empty<string>()); var mediaEncoder = Mock.Of<IMediaEncoder>(MockBehavior.Strict); @@ -341,9 +341,9 @@ public class MediaInfoResolverTests } var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); - directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>())) .Returns(files); - directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>())) .Returns(Array.Empty<string>()); List<MediaStream> GenerateMediaStreams() @@ -413,16 +413,16 @@ public class MediaInfoResolverTests var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); if (useMetadataDirectory) { - directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>())) .Returns(Array.Empty<string>()); - directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>())) .Returns(new[] { MetadataDirectoryPath + "/" + file }); } else { - directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>())) .Returns(new[] { VideoDirectoryPath + "/" + file }); - directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>())) + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>())) .Returns(Array.Empty<string>()); } diff --git a/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs new file mode 100644 index 0000000000..8f5b1b3c48 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs @@ -0,0 +1,110 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.TV; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Providers.Tests.TV; + +public class EpisodeMetadataServiceTests +{ + private readonly TestEpisodeMetadataService _service = new(); + + [Fact] + public void MergeData_ProviderSeasonOverridesPathDerivedSeason() + { + var source = new MetadataResult<Episode> + { + Item = new Episode + { + ParentIndexNumber = 2 + } + }; + + var target = new MetadataResult<Episode> + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + _service.Merge(source, target, replaceData: false, mergeMetadataSettings: true); + + Assert.Equal(2, target.Item.ParentIndexNumber); + } + + [Fact] + public void MergeData_BackfillExistingMetadata_DoesNotOverrideProviderSeason() + { + var existingMetadata = new MetadataResult<Episode> + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + var temp = new MetadataResult<Episode> + { + Item = new Episode + { + ParentIndexNumber = 2 + } + }; + + _service.Merge(existingMetadata, temp, replaceData: false, mergeMetadataSettings: false); + + Assert.Equal(2, temp.Item.ParentIndexNumber); + } + + [Fact] + public void MergeData_MissingProviderSeasonKeepsExistingSeason() + { + var source = new MetadataResult<Episode> + { + Item = new Episode() + }; + + var target = new MetadataResult<Episode> + { + Item = new Episode + { + ParentIndexNumber = 1 + } + }; + + _service.Merge(source, target, replaceData: false, mergeMetadataSettings: true); + + Assert.Equal(1, target.Item.ParentIndexNumber); + } + + private sealed class TestEpisodeMetadataService : EpisodeMetadataService + { + public TestEpisodeMetadataService() + : base( + Mock.Of<IServerConfigurationManager>(), + NullLogger<EpisodeMetadataService>.Instance, + Mock.Of<IProviderManager>(), + Mock.Of<IFileSystem>(), + Mock.Of<ILibraryManager>(), + Mock.Of<IExternalDataManager>(), + Mock.Of<IItemRepository>()) + { + } + + public void Merge(MetadataResult<Episode> source, MetadataResult<Episode> target, bool replaceData, bool mergeMetadataSettings) + { + MergeData(source, target, Array.Empty<MetadataField>(), replaceData, mergeMetadataSettings); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index a7bbef7ed4..03c0b4af39 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,4 +1,9 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Emby.Server.Implementations.Library; +using MediaBrowser.Model.IO; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; @@ -78,4 +83,391 @@ public class DotIgnoreIgnoreRuleTest // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } + + [Fact] + public void CacheHit_RepeatedCallsDoNotRereadFiles() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var subDir = Path.Combine(tempDir, "subdir"); + Directory.CreateDirectory(subDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(subDir, "test.tmp"), + IsDirectory = false + }; + + // First call - should cache + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Second call - should use cache + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result2); + + // Third call with different file in same directory - should use cache + var fileInfo2 = new FileSystemMetadata + { + FullName = Path.Combine(subDir, "other.tmp"), + IsDirectory = false + }; + var result3 = rule.ShouldIgnore(fileInfo2, null); + Assert.True(result3); + + // Call with file that doesn't match pattern + var fileInfo3 = new FileSystemMetadata + { + FullName = Path.Combine(subDir, "other.txt"), + IsDirectory = false + }; + var result4 = rule.ShouldIgnore(fileInfo3, null); + Assert.False(result4); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void CacheInvalidation_ModifyIgnoreFile_Reparses() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.tmp"), + IsDirectory = false + }; + + // First call - should ignore .tmp files + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Modify the .ignore file to ignore .txt instead + // Wait a bit to ensure the file modification time changes + Thread.Sleep(50); + File.WriteAllText(ignoreFilePath, "*.txt"); + + // Now .tmp files should NOT be ignored + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.False(result2); + + // And .txt files SHOULD be ignored + var txtFileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.txt"), + IsDirectory = false + }; + var result3 = rule.ShouldIgnore(txtFileInfo, null); + Assert.True(result3); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void EmptyIgnoreFile_IgnoresEverything() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, string.Empty); + + var rule = new DotIgnoreIgnoreRule(); + + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "anyfile.mkv"), + IsDirectory = false + }; + + // Empty .ignore file should ignore everything + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void WhitespaceOnlyIgnoreFile_IgnoresEverything() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, " \n\t\n "); + + var rule = new DotIgnoreIgnoreRule(); + + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "anyfile.mkv"), + IsDirectory = false + }; + + // Whitespace-only .ignore file should ignore everything + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void NoIgnoreFile_DoesNotIgnore() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var rule = new DotIgnoreIgnoreRule(); + + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "anyfile.mkv"), + IsDirectory = false + }; + + // No .ignore file means don't ignore + var result = rule.ShouldIgnore(fileInfo, null); + Assert.False(result); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void ConcurrentAccess_ThreadSafe() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + + // Run multiple parallel checks + Parallel.For(0, 100, i => + { + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, $"test{i}.tmp"), + IsDirectory = false + }; + + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + }); + + // Also test with non-matching files + Parallel.For(0, 100, i => + { + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, $"test{i}.txt"), + IsDirectory = false + }; + + var result = rule.ShouldIgnore(fileInfo, null); + Assert.False(result); + }); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void ClearCache_ClearsAllCachedData() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.tmp"), + IsDirectory = false + }; + + // First call to populate cache + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Clear cache + rule.ClearDirectoryCache(); + + // Should still work (will re-populate cache) + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result2); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void IgnoreFileDeleted_HandlesGracefully() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "test.tmp"), + IsDirectory = false + }; + + // First call - should ignore + var result1 = rule.ShouldIgnore(fileInfo, null); + Assert.True(result1); + + // Delete the .ignore file + File.Delete(ignoreFilePath); + + // Should not ignore anymore (file deleted) + var result2 = rule.ShouldIgnore(fileInfo, null); + Assert.False(result2); + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + + [Fact] + public void ParentDirectoryIgnoreFile_AppliesToSubdirectories() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var subDir1 = Path.Combine(tempDir, "sub1"); + var subDir2 = Path.Combine(tempDir, "sub1", "sub2"); + Directory.CreateDirectory(subDir1); + Directory.CreateDirectory(subDir2); + + try + { + // Put .ignore in root + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "*.tmp"); + + var rule = new DotIgnoreIgnoreRule(); + + // Check file in sub2 - should find .ignore in parent + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(subDir2, "test.tmp"), + IsDirectory = false + }; + + var result = rule.ShouldIgnore(fileInfo, null); + Assert.True(result); + + // Check file in sub1 + var fileInfo2 = new FileSystemMetadata + { + FullName = Path.Combine(subDir1, "test.tmp"), + IsDirectory = false + }; + + var result2 = rule.ShouldIgnore(fileInfo2, null); + Assert.True(result2); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void DirectoryMatching_TrailingSlashPattern() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var subDir = Path.Combine(tempDir, "videos"); + Directory.CreateDirectory(subDir); + + try + { + var ignoreFilePath = Path.Combine(tempDir, ".ignore"); + File.WriteAllText(ignoreFilePath, "videos/"); + + var rule = new DotIgnoreIgnoreRule(); + + // Directory should be ignored + var dirInfo = new FileSystemMetadata + { + FullName = subDir, + IsDirectory = true + }; + + var result = rule.ShouldIgnore(dirInfo, null); + Assert.True(result); + + // File named "videos" should NOT be ignored (pattern has trailing slash) + var fileInfo = new FileSystemMetadata + { + FullName = Path.Combine(tempDir, "videos"), + IsDirectory = false + }; + + // Note: The Ignore library behavior may vary here, this tests the actual behavior + var resultFile = rule.ShouldIgnore(fileInfo, null); + // The file named "videos" without trailing slash might or might not match depending on the library + // This test documents the actual behavior + } + finally + { + Directory.Delete(tempDir, true); + } + } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index cc2e47c33a..16b601dc3c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -61,6 +61,94 @@ namespace Jellyfin.Server.Implementations.Tests.Library Assert.NotNull(episodeResolver.Resolve(itemResolveArgs)); } + [Theory] + [InlineData("/media/Show/Season 01/Show S01E01 [tvdbid=12345].mkv", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01/Show S01E01 [tvdbid-12345].mkv", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01/Show S01E01 (tvdbid=12345).mkv", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 02/Show S02E03 [tvmazeid=67890].mkv", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 02/Show S02E03 [tvmazeid-67890].mkv", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 03/Show S03E04 [tmdbid=99999].mkv", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show/Season 03/Show S03E04 [tmdbid-99999].mkv", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show/Season 04/Show S04E05 [imdbid=tt1234567].mkv", MetadataProvider.Imdb, "tt1234567")] + [InlineData("/media/Show/Season 04/Show S04E05 [imdbid-tt1234567].mkv", MetadataProvider.Imdb, "tt1234567")] + public void Resolve_EpisodeFileWithProviderId_SetsProviderId(string path, MetadataProvider provider, string expectedId) + { + var series = new Series { Name = "Show" }; + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + CollectionType = CollectionType.tvshows, + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = false + } + }; + + var episode = episodeResolver.Resolve(itemResolveArgs); + + Assert.NotNull(episode); + Assert.True(episode.TryGetProviderId(provider, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Fact] + public void Resolve_EpisodeFileWithProviderIdsOnAllLevels_OnlyUsesEpisodeLevelId() + { + // Series folder has tvdbid=11111, season folder has tvdbid=22222, episode file has tvdbid=33333. + // The episode should only pick up its own ID, not the series- or season-level ones. + var series = new Series { Name = "Show" }; + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + CollectionType = CollectionType.tvshows, + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show [tvdbid=11111]/Season 01 [tvdbid=22222]/Show S01E01 [tvdbid=33333].mkv", + IsDirectory = false + } + }; + + var episode = episodeResolver.Resolve(itemResolveArgs); + + Assert.NotNull(episode); + Assert.True(episode.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("33333", tvdbId); + } + + [Fact] + public void Resolve_EpisodeFileWithMultipleProviderIds_SetsAll() + { + var series = new Series { Name = "Show" }; + var episodeResolver = new EpisodeResolverMock(Mock.Of<ILogger<EpisodeResolver>>(), _namingOptions, Mock.Of<IDirectoryService>()); + var itemResolveArgs = new ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + CollectionType = CollectionType.tvshows, + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show/Season 01/Show S01E01 [tvdbid=12345][tmdbid=99999].mkv", + IsDirectory = false + } + }; + + var episode = episodeResolver.Resolve(itemResolveArgs); + + Assert.NotNull(episode); + Assert.True(episode.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("12345", tvdbId); + Assert.True(episode.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)); + Assert.Equal("99999", tmdbId); + } + private sealed class EpisodeResolverMock : EpisodeResolver { public EpisodeResolverMock(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService) : base(logger, namingOptions, directoryService) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs index 8ed3d8b944..facdb2bc2e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs @@ -1,9 +1,18 @@ +using System; using AutoFixture; using AutoFixture.AutoMoq; +using Castle.Components.DictionaryAdapter; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; +using Jellyfin.Database.Implementations.Entities; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; +using Moq; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library @@ -11,12 +20,28 @@ namespace Jellyfin.Server.Implementations.Tests.Library public class MediaSourceManagerTests { private readonly MediaSourceManager _mediaSourceManager; + private readonly Mock<IUserDataManager> _mockUserDataManager; + private readonly Mock<ILocalizationManager> _mockLocalizationManager; + private Video _item; + private User _user; public MediaSourceManagerTests() { IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); fixture.Inject<IFileSystem>(fixture.Create<ManagedFileSystem>()); + + _mockUserDataManager = fixture.Freeze<Mock<IUserDataManager>>(); + _mockUserDataManager.Setup(m => m.GetUserData(It.IsAny<User>(), It.IsAny<BaseItem>())).Returns(new UserItemData() { Key = "key" }); + + _mockLocalizationManager = fixture.Create<Mock<ILocalizationManager>>(); + _mockLocalizationManager.Setup(m => m.FindLanguageInfo(It.IsAny<string>())).Returns((string s) => string.IsNullOrEmpty(s) ? null : new CultureDto(s, s, s, new EditableList<string> { s })); + fixture.Inject(_mockLocalizationManager.Object); + _mediaSourceManager = fixture.Create<MediaSourceManager>(); + + _item = new Video { Id = Guid.NewGuid(), OwnerId = Guid.Empty, ParentId = Guid.Empty }; + + _user = fixture.Create<User>(); } [Theory] @@ -28,5 +53,96 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("rtsp://media.example.com:554/twister/audiotrack", MediaProtocol.Rtsp)] public void GetPathProtocol_ValidArg_Correct(string path, MediaProtocol expected) => Assert.Equal(expected, _mediaSourceManager.GetPathProtocol(path)); + + [Theory] + [InlineData(5, "eng", "eng", false, true)] + [InlineData(5, "eng", "eng", true, true)] + [InlineData(2, "ger", "eng", false, true)] + [InlineData(2, "ger", "eng", true, true)] + [InlineData(1, "fre", "eng", false, true)] + [InlineData(2, "fre", "eng", true, true)] + [InlineData(5, "OriginalLanguage", "eng", false, false)] + [InlineData(4, "OriginalLanguage", "eng", false, true)] + [InlineData(5, "OriginalLanguage", "eng", true, false)] + [InlineData(5, "OriginalLanguage", "eng", true, true)] + [InlineData(2, "OriginalLanguage", "jpn", true, true)] + [InlineData(2, "OriginalLanguage", "jpn", false, true)] + [InlineData(2, "OriginalLanguage", "jpn,eng", false, true)] + [InlineData(4, "OriginalLanguage", null, false, true)] + [InlineData(2, "OriginalLanguage", null, true, true)] + [InlineData(4, "OriginalLanguage", "", false, true)] + [InlineData(2, "OriginalLanguage", "", false, false)] + [InlineData(2, "OriginalLanguage", "ger", false, true)] + [InlineData(2, "OriginalLanguage", "ger", false, false)] + [InlineData(1, "OriginalLanguage", "fre", false, false)] + [InlineData(2, "OriginalLanguage", "fre", true, true)] + [InlineData(2, "OriginalLanguage", "fre", true, false)] + public void SetDefaultAudioStreamIndex_Index_Correct( + int expectedIndex, + string prefferedLanguage, + string? originalLanguage, + bool playDefault, + bool originalExist) + { + var streams = new MediaStream[] + { + new() + { + Index = 0, + Type = MediaStreamType.Video, + IsDefault = true + }, + new() + { + Index = 1, + Type = MediaStreamType.Audio, + Language = "fre", + IsDefault = false, + IsOriginal = false + }, + new() + { + Index = 2, + Type = MediaStreamType.Audio, + Language = "jpn", + IsDefault = true, + IsOriginal = false + }, + new() + { + Index = 3, + Type = MediaStreamType.Audio, + Language = "eng", + IsDefault = false, + IsOriginal = false + }, + new() + { + Index = 4, + Type = MediaStreamType.Audio, + Language = "eng", + IsDefault = false, + IsOriginal = originalExist, + }, + new() + { + Index = 5, + Type = MediaStreamType.Audio, + Language = "eng", + IsDefault = true, + IsOriginal = false, + } + }; + var mediaInfo = new MediaSourceInfo + { + MediaStreams = streams + }; + _user.AudioLanguagePreference = prefferedLanguage; + _user.PlayDefaultAudioTrack = playDefault; + _item.OriginalLanguage = originalLanguage; + + _mediaSourceManager.SetDefaultAudioAndSubtitleStreamIndices(_item, mediaInfo, _user); + Assert.Equal(expectedIndex, mediaInfo.DefaultAudioStreamIndex); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs new file mode 100644 index 0000000000..133a3f7d47 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/SeasonResolverTests.cs @@ -0,0 +1,145 @@ +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.TV; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class SeasonResolverTests + { + private static readonly NamingOptions _namingOptions = new(); + private readonly SeasonResolver _resolver; + + public SeasonResolverTests() + { + var localizationMock = new Mock<ILocalizationManager>(); + localizationMock + .Setup(l => l.GetLocalizedString(It.IsAny<string>())) + .Returns("Season {0}"); + + _resolver = new SeasonResolver( + _namingOptions, + localizationMock.Object, + Mock.Of<ILogger<SeasonResolver>>()); + } + + [Theory] + [InlineData("/media/Show/Season 01 [tvdbid=12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01 [tvdbid-12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 01 (tvdbid=12345)", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show/Season 02 [tvmazeid=67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 02 [tvmazeid-67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show/Season 03 [tmdbid=99999]", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show/Season 03 [tmdbid-99999]", MetadataProvider.Tmdb, "99999")] + public void Resolve_SeasonFolderWithProviderId_SetsProviderId(string path, MetadataProvider provider, string expectedId) + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(provider, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Fact] + public void Resolve_SeasonFolderWithMultipleProviderIds_SetsAll() + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show/Season 01 [tvdbid=12345][tmdbid=99999]", + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("12345", tvdbId); + Assert.True(season.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)); + Assert.Equal("99999", tmdbId); + } + + [Fact] + public void Resolve_SeasonFolderWithSeriesProviderIdInParentPath_DoesNotInheritSeriesId() + { + // Series folder has tvdbid=11111, season folder has tvdbid=22222. + // The season should only pick up its own ID, not the series-level one. + var series = new Series { Path = "/media/Show [tvdbid=11111]" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show [tvdbid=11111]/Season 01 [tvdbid=22222]", + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.True(season.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("22222", tvdbId); + } + + [Fact] + public void Resolve_SeasonFolderWithNoProviderId_HasNoProviderIds() + { + var series = new Series { Path = "/media/Show" }; + + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + null) + { + Parent = series, + LibraryOptions = new LibraryOptions(), + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show/Season 01", + IsDirectory = true + } + }; + + var season = _resolver.Resolve(args); + + Assert.NotNull(season); + Assert.False(season.TryGetProviderId(MetadataProvider.Tvdb, out _)); + Assert.False(season.TryGetProviderId(MetadataProvider.TvMaze, out _)); + Assert.False(season.TryGetProviderId(MetadataProvider.Tmdb, out _)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs new file mode 100644 index 0000000000..8dbd5f5b41 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/SeriesResolverTests.cs @@ -0,0 +1,124 @@ +using Emby.Naming.Common; +using Emby.Server.Implementations.Library.Resolvers.TV; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class SeriesResolverTests + { + private static readonly NamingOptions _namingOptions = new(); + private readonly SeriesResolver _resolver; + private readonly Mock<ILibraryManager> _libraryManagerMock; + + public SeriesResolverTests() + { + _libraryManagerMock = new Mock<ILibraryManager>(); + // Return null so that configuredContentType != CollectionType.tvshows, allowing series resolution. + _libraryManagerMock + .Setup(m => m.GetConfiguredContentType(It.IsAny<string>())) + .Returns((CollectionType?)null); + + _resolver = new SeriesResolver(Mock.Of<ILogger<SeriesResolver>>(), _namingOptions); + } + + private MediaBrowser.Controller.Library.ItemResolveArgs MakeTvArgs(string path) => + new(Mock.Of<IServerApplicationPaths>(), _libraryManagerMock.Object) + { + CollectionType = CollectionType.tvshows, + FileSystemChildren = [], + FileInfo = new FileSystemMetadata + { + FullName = path, + IsDirectory = true + } + }; + + [Theory] + [InlineData("/media/Show [tvdbid=12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show [tvdbid-12345]", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show (tvdbid=12345)", MetadataProvider.Tvdb, "12345")] + [InlineData("/media/Show [tvmazeid=67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show [tvmazeid-67890]", MetadataProvider.TvMaze, "67890")] + [InlineData("/media/Show [tmdbid=99999]", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show [tmdbid-99999]", MetadataProvider.Tmdb, "99999")] + [InlineData("/media/Show [imdbid=tt1234567]", MetadataProvider.Imdb, "tt1234567")] + [InlineData("/media/Show [imdbid-tt1234567]", MetadataProvider.Imdb, "tt1234567")] + public void ResolvePath_SeriesFolderWithProviderId_SetsProviderId(string path, MetadataProvider provider, string expectedId) + { + var series = _resolver.ResolvePath(MakeTvArgs(path)) as Series; + + Assert.NotNull(series); + Assert.True(series.TryGetProviderId(provider, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Theory] + [InlineData("/media/Show [anidbid=11111]", "AniDB", "11111")] + [InlineData("/media/Show [anilistid=22222]", "AniList", "22222")] + [InlineData("/media/Show [anisearchid=33333]", "AniSearch", "33333")] + public void ResolvePath_SeriesFolderWithAniProviderId_SetsProviderId(string path, string providerKey, string expectedId) + { + var series = _resolver.ResolvePath(MakeTvArgs(path)) as Series; + + Assert.NotNull(series); + Assert.True(series.TryGetProviderId(providerKey, out var actualId)); + Assert.Equal(expectedId, actualId); + } + + [Fact] + public void ResolvePath_SeriesFolderWithMultipleProviderIds_SetsAll() + { + var series = _resolver.ResolvePath(MakeTvArgs("/media/Show [tvdbid=12345][tmdbid=99999]")) as Series; + + Assert.NotNull(series); + Assert.True(series.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)); + Assert.Equal("12345", tvdbId); + Assert.True(series.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbId)); + Assert.Equal("99999", tmdbId); + } + + [Fact] + public void ResolvePath_SeriesFolderWithNoProviderId_HasNoProviderIds() + { + var series = _resolver.ResolvePath(MakeTvArgs("/media/Show")) as Series; + + Assert.NotNull(series); + Assert.False(series.TryGetProviderId(MetadataProvider.Tvdb, out _)); + Assert.False(series.TryGetProviderId(MetadataProvider.TvMaze, out _)); + Assert.False(series.TryGetProviderId(MetadataProvider.Tmdb, out _)); + Assert.False(series.TryGetProviderId(MetadataProvider.Imdb, out _)); + Assert.False(series.TryGetProviderId("AniDB", out _)); + Assert.False(series.TryGetProviderId("AniList", out _)); + Assert.False(series.TryGetProviderId("AniSearch", out _)); + } + + [Fact] + public void ResolvePath_SeriesFolderNotInTvShowsCollection_DoesNotResolve() + { + // Without CollectionType.tvshows, a plain folder with no tvshow.nfo and + // no season/episode children should not resolve as a Series. + var args = new MediaBrowser.Controller.Library.ItemResolveArgs( + Mock.Of<IServerApplicationPaths>(), + _libraryManagerMock.Object) + { + CollectionType = null, + FileSystemChildren = [], + FileInfo = new FileSystemMetadata + { + FullName = "/media/Show [tvdbid=12345]", + IsDirectory = true + } + }; + + Assert.Null(_resolver.ResolvePath(args)); + } + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 5bcfc580ff..acabaf3acb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -242,6 +242,40 @@ namespace Jellyfin.Server.Implementations.Tests.Localization } [Theory] + [InlineData("US:INVALID", "US")] // Colon separator, known country code, unknown rating + [InlineData("us:INVALID", "US")] // Colon separator, lowercase country code + [InlineData("DE-INVALID", "US")] // Hyphen separator, known language prefix, unknown rating + [InlineData("ca:INVALID", "US")] // Colon separator, known country code (Canada) + public async Task GetRatingScore_UnknownRatingWithKnownCountry_ReturnsNull(string rating, string countryCode) + { + var localizationManager = Setup(new ServerConfiguration + { + MetadataCountryCode = countryCode + }); + await localizationManager.LoadAll(); + + Assert.Null(localizationManager.GetRatingScore(rating)); + } + + [Theory] + [InlineData("us:R", "DE", 17, 0)] // Colon separator, explicit US country, valid US rating + [InlineData("US:PG-13", "DE", 13, 0)] // Colon separator, explicit US country, valid US rating + [InlineData("ca:R", "US", 18, 1)] // Colon separator, Canada country code, valid CA rating + public async Task GetRatingScore_ValidRatingWithCountrySeparator_ReturnsScore(string rating, string countryCode, int expectedScore, int? expectedSubScore) + { + var localizationManager = Setup(new ServerConfiguration + { + MetadataCountryCode = countryCode + }); + await localizationManager.LoadAll(); + + var score = localizationManager.GetRatingScore(rating); + Assert.NotNull(score); + Assert.Equal(expectedScore, score.Score); + Assert.Equal(expectedSubScore, score.SubScore); + } + + [Theory] [InlineData("Default", "Default")] [InlineData("HeaderLiveTV", "Live TV")] public void GetLocalizedString_Valid_Success(string key, string expected) diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 0952fb8b63..54f443de2d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -111,7 +111,7 @@ namespace Jellyfin.Server.Integration.Tests var appHost = (TestAppHost)host.Services.GetRequiredService<IApplicationHost>(); appHost.ServiceProvider = host.Services; var applicationPaths = appHost.ServiceProvider.GetRequiredService<IApplicationPaths>(); - Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>()).GetAwaiter().GetResult(); + Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>(), new()).GetAwaiter().GetResult(); Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisation).GetAwaiter().GetResult(); appHost.InitializeServices(Mock.Of<IConfiguration>()).GetAwaiter().GetResult(); Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult(); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 1e8652f4b9..4142831c31 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -294,5 +294,48 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers // Verify that the lowercase "tmdbcol" is NOT in the provider IDs Assert.False(item.ProviderIds.ContainsKey("tmdbcol")); } + + [Fact] + public void Parse_CommunityRating_ValidRating_Success() + { + var result = new MetadataResult<Video>() + { + Item = new Movie() + }; + + _parser.Fetch(result, "Test Data/CommunityRating.nfo", CancellationToken.None); + var item = (Movie)result.Item; + + Assert.Equal(7.5f, item.CommunityRating); + } + + [Fact] + public void Parse_CommunityRating_OutOfRange_Ignored() + { + var result = new MetadataResult<Video>() + { + Item = new Movie() + }; + + _parser.Fetch(result, "Test Data/CommunityRating_OutOfRange.nfo", CancellationToken.None); + var item = (Movie)result.Item; + + // Rating should not be set if outside 0-10 range + Assert.Null(item.CommunityRating); + } + + [Fact] + public void Parse_CommunityRating_Comma() + { + var result = new MetadataResult<Video>() + { + Item = new Movie() + }; + + _parser.Fetch(result, "Test Data/CommunityRating_Comma.nfo", CancellationToken.None); + var item = (Movie)result.Item; + + Assert.Equal(7.5f, item.CommunityRating); + } } } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating.nfo new file mode 100644 index 0000000000..387de10c0e --- /dev/null +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating.nfo @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<movie> + <title>Test Movie</title> + <communityrating>7.5</communityrating> +</movie>
\ No newline at end of file diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating_Comma.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating_Comma.nfo new file mode 100644 index 0000000000..4ec215e2e1 --- /dev/null +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating_Comma.nfo @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<movie> + <title>Test Movie</title> + <communityrating>7,5</communityrating> +</movie>
\ No newline at end of file diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating_OutOfRange.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating_OutOfRange.nfo new file mode 100644 index 0000000000..126854edd3 --- /dev/null +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/CommunityRating_OutOfRange.nfo @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<movie> + <title>Test Movie</title> + <communityrating>15.5</communityrating> +</movie>
\ No newline at end of file |
