aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorMBR-0001 <55142207+MBR-0001@users.noreply.github.com>2026-04-18 17:14:31 +0200
committerGitHub <noreply@github.com>2026-04-18 17:14:31 +0200
commita2eff41d6617bed7a7c5518d00f227a2406a8421 (patch)
tree933386669537d5145ca6b3706728454aff27d703 /tests
parentd4a46bc6291560b35a9b64fb88b68019f903d0ef (diff)
parent5aa093d2997d8120ce19037642556070d27ce0bb (diff)
Merge branch 'master' into removeGlobalSubtitleConfiguration
Diffstat (limited to 'tests')
-rw-r--r--tests/Directory.Build.props2
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj1
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs26
-rw-r--r--tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs34
-rw-r--r--tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj2
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs32
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs282
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json137
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs55
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs6
-rw-r--r--tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs43
-rw-r--r--tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs116
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json3
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json2
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json11
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json7
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json3
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json3
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json3
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json6
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json6
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json7
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json13
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json13
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json4
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json4
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json3
-rw-r--r--tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json3
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs1
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs2
-rw-r--r--tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs44
-rw-r--r--tests/Jellyfin.Networking.Tests/NetworkParseTests.cs10
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs102
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs109
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs72
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs87
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs19
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs42
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs15
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj1
-rw-r--r--tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj1
-rw-r--r--tests/Jellyfin.Server.Tests/ParseNetworkTests.cs7
43 files changed, 1160 insertions, 181 deletions
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
index 6b851021f9..feec35307c 100644
--- a/tests/Directory.Build.props
+++ b/tests/Directory.Build.props
@@ -4,7 +4,7 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index 0150189108..6b84c4438f 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -10,7 +10,6 @@
<PackageReference Include="AutoFixture.AutoMoq" />
<PackageReference Include="AutoFixture.Xunit2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
- <PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
diff --git a/tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs b/tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs
new file mode 100644
index 0000000000..7093b25006
--- /dev/null
+++ b/tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs
@@ -0,0 +1,26 @@
+using System;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Querying;
+using Xunit;
+
+namespace Jellyfin.Controller.Tests.Entities;
+
+public class InternalItemsQueryTests
+{
+ public static TheoryData<ItemFilter[]> ApplyFilters_Invalid()
+ {
+ var data = new TheoryData<ItemFilter[]>();
+ data.Add([ItemFilter.IsFolder, ItemFilter.IsNotFolder]);
+ data.Add([ItemFilter.IsPlayed, ItemFilter.IsUnplayed]);
+ data.Add([ItemFilter.Likes, ItemFilter.Dislikes]);
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(ApplyFilters_Invalid))]
+ public void ApplyFilters_Invalid_ThrowsArgumentException(ItemFilter[] filters)
+ {
+ var query = new InternalItemsQuery();
+ Assert.Throws<ArgumentException>(() => query.ApplyFilters(filters));
+ }
+}
diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
deleted file mode 100644
index 105e2a52ae..0000000000
--- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Linq;
-using Xunit;
-
-namespace Jellyfin.Extensions.Tests
-{
- public class AlphanumericComparatorTests
- {
- // InlineData is pre-sorted
- [Theory]
- [InlineData(null, "", "1", "9", "10", "a", "z")]
- [InlineData("50F", "100F", "SR9", "SR100")]
- [InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")]
- [InlineData("Hard drive 2GB", "Hard drive 20GB")]
- [InlineData("b", "e", "è", "ě", "f", "g", "k")]
- [InlineData("123456789", "123456789a", "abc", "abcd")]
- [InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")]
- [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")]
- [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")]
- [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")]
- [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")]
- [InlineData("a5", "a11")]
- [InlineData("a05a", "a5b")]
- [InlineData("a5a", "a05b")]
- [InlineData("6xxx", "007asdf")]
- [InlineData("00042Q", "42s")]
- public void AlphanumericComparatorTest(params string?[] strings)
- {
- var copy = strings.Reverse().ToArray();
- Array.Sort(copy, new AlphanumericComparator());
- Assert.Equal(strings, copy);
- }
- }
-}
diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
index fdcf7d61e0..bdf6bc383a 100644
--- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
+++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 94710a0957..3369af0e84 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -39,6 +39,23 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
public void GetFrameRate_Success(string value, float? expected)
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
+ [Theory]
+ [InlineData("1:1", true)]
+ [InlineData("3201:3200", true)]
+ [InlineData("1215:1216", true)]
+ [InlineData("1001:1000", true)]
+ [InlineData("16:15", false)]
+ [InlineData("8:9", false)]
+ [InlineData("32:27", false)]
+ [InlineData("10:11", false)]
+ [InlineData("64:45", false)]
+ [InlineData("4:3", false)]
+ [InlineData("0:1", false)]
+ [InlineData("", false)]
+ [InlineData(null, false)]
+ public void IsNearSquarePixelSar_DetectsCorrectly(string? sar, bool expected)
+ => Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar));
+
[Fact]
public void GetMediaInfo_MetaData_Success()
{
@@ -123,6 +140,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal(358, res.VideoStream.Height);
Assert.Equal(720, res.VideoStream.Width);
Assert.Equal("2.40:1", res.VideoStream.AspectRatio);
+ Assert.True(res.VideoStream.IsAnamorphic); // SAR 32:27 — genuinely anamorphic NTSC DVD 16:9
Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
Assert.Equal(31d, res.VideoStream.Level);
Assert.Equal(1, res.VideoStream.RefFrames);
@@ -191,8 +209,20 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.Equal("mkv,webm", res.Container);
Assert.Equal(2, res.MediaStreams.Count);
+ Assert.Equal(540, res.MediaStreams[0].Width);
+ Assert.Equal(360, res.MediaStreams[0].Height);
+ }
- Assert.False(res.MediaStreams[0].IsAVC);
+ [Fact]
+ public void GetMediaInfo_WebM_Like_Mkv()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_web_like_mkv_with_subtitle.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
+
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
+
+ Assert.Equal("mkv", res.Container);
+ Assert.Equal(3, res.MediaStreams.Count);
}
[Fact]
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
new file mode 100644
index 0000000000..5f84e85592
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
@@ -0,0 +1,282 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.MediaInfo;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class FilterEventsTests
+ {
+ private readonly SubtitleEncoder _encoder;
+
+ public FilterEventsTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ _encoder = fixture.Create<SubtitleEncoder>();
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained()
+ {
+ // Subtitle starts at 5s, ends at 15s.
+ // Segment requested from 10s to 20s.
+ // The subtitle is still on screen at 10s and should NOT be dropped.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Still on screen")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("2", "Next subtitle")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ Assert.Equal("1", track.TrackEvents[0].Id);
+ Assert.Equal("2", track.TrackEvents[1].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped()
+ {
+ // Subtitle starts at 2s, ends at 5s.
+ // Segment requested from 10s.
+ // The subtitle ended before the segment — should be dropped.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Already gone")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(5).Ticks
+ },
+ new SubtitleTrackEvent("2", "Visible")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Single(track.TrackEvents);
+ Assert.Equal("2", track.TrackEvents[0].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleAfterSegment_IsDropped()
+ {
+ // Segment is 10s-20s, subtitle starts at 25s.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "In range")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("2", "After segment")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(25).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(30).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Single(track.TrackEvents);
+ Assert.Equal("1", track.TrackEvents[0].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps()
+ {
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Subtitle")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(15).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(20).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
+ preserveTimestamps: false);
+
+ Assert.Single(track.TrackEvents);
+ // Timestamps should be shifted back by 10s
+ Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks);
+ Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks);
+ }
+
+ [Fact]
+ public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps()
+ {
+ var startTicks = TimeSpan.FromSeconds(15).Ticks;
+ var endTicks = TimeSpan.FromSeconds(20).Ticks;
+
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Subtitle")
+ {
+ StartPositionTicks = startTicks,
+ EndPositionTicks = endTicks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Single(track.TrackEvents);
+ Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks);
+ Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks);
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained()
+ {
+ // Subtitle ends exactly when the segment begins.
+ // EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0,
+ // so SkipWhile stops and the subtitle is retained.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Boundary subtitle")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(10).Ticks
+ },
+ new SubtitleTrackEvent("2", "In range")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ Assert.Equal("1", track.TrackEvents[0].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps()
+ {
+ // Subtitle starts at 5s, ends at 15s.
+ // Segment requested from 10s to 20s, preserveTimestamps = false.
+ // The subtitle spans the boundary and is retained, but shifting
+ // StartPositionTicks by -10s would produce -5s (negative).
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Spans boundary")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("2", "Fully in range")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: false);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ // Subtitle 1: start should be clamped to 0, not -5s
+ Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative");
+ Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks);
+ // Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s)
+ Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks);
+ Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks);
+ }
+
+ [Fact]
+ public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition()
+ {
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Before")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(4).Ticks
+ },
+ new SubtitleTrackEvent("2", "After")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("3", "Much later")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(500).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(505).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: 0,
+ preserveTimestamps: true);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ Assert.Equal("2", track.TrackEvents[0].Id);
+ Assert.Equal("3", track.TrackEvents[1].Id);
+ }
+ }
+}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json
new file mode 100644
index 0000000000..4f52dd90dc
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json
@@ -0,0 +1,137 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "vp8",
+ "codec_long_name": "On2 VP8",
+ "profile": "1",
+ "codec_type": "video",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 540,
+ "height": 360,
+ "coded_width": 540,
+ "coded_height": 360,
+ "closed_captions": 0,
+ "film_grain": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "3:2",
+ "pix_fmt": "yuv420p",
+ "level": -99,
+ "field_order": "progressive",
+ "refs": 1,
+ "r_frame_rate": "2997/125",
+ "avg_frame_rate": "2997/125",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "vorbis",
+ "codec_long_name": "Vorbis",
+ "codec_type": "audio",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "fltp",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration": "117.707000",
+ "bit_rate": "127998",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ },
+ {
+ "index": 2,
+ "codec_name": "subrip",
+ "codec_long_name": "SubRip subtitle",
+ "codec_type": "subtitle",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ }
+ ],
+ "format": {
+ "filename": "sample.mkv",
+ "nb_streams": 3,
+ "nb_programs": 0,
+ "format_name": "matroska,webm",
+ "format_long_name": "Matroska / WebM",
+ "start_time": "0.000000",
+ "duration": "117.700914",
+ "size": "8566268",
+ "bit_rate": "582239",
+ "probe_score": 100
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs
index e32baef55d..6436d7d0e4 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs
@@ -134,8 +134,6 @@ public class LegacyStreamInfo : StreamInfo
{
list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
}
-
- list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
}
else
{
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 2c1080ffe3..8269ae58cd 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests
return (path, query, filename, extension);
}
+
+ [Theory]
+ // EnableSubtitleExtraction = false, internal subtitles
+ [InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ [InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ [InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ // EnableSubtitleExtraction = false, external subtitles
+ [InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ // EnableSubtitleExtraction = true, internal subtitles
+ [InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ // EnableSubtitleExtraction = true, external subtitles
+ [InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ public void GetSubtitleProfile_RespectsExtractionSetting(
+ string codec,
+ string profileFormat,
+ bool enableSubtitleExtraction,
+ bool isExternal,
+ PlayMethod playMethod,
+ SubtitleDeliveryMethod expectedMethod)
+ {
+ var mediaSource = new MediaSourceInfo();
+ var subtitleStream = new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Index = 0,
+ IsExternal = isExternal,
+ Path = isExternal ? "/media/sub." + codec : null,
+ Codec = codec,
+ SupportsExternalStream = MediaStream.IsTextFormat(codec)
+ };
+
+ var subtitleProfiles = new[]
+ {
+ new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External }
+ };
+
+ var transcoderSupport = new Mock<ITranscoderSupport>();
+ transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny<string>())).Returns(enableSubtitleExtraction);
+
+ var result = StreamBuilder.GetSubtitleProfile(
+ mediaSource,
+ subtitleStream,
+ subtitleProfiles,
+ playMethod,
+ transcoderSupport.Object,
+ null,
+ null);
+
+ Assert.Equal(expectedMethod, result.Method);
+ }
}
}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
index 8dea468064..4b3126fe11 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
@@ -216,8 +216,7 @@ public class StreamInfoTests
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
- // New version will return and & after the ? due to optional parameters.
- string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
+ string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
}
@@ -234,8 +233,7 @@ public class StreamInfoTests
FillAllProperties(streamInfo);
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
- // New version will return and & after the ? due to optional parameters.
- string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
+ string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
}
diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
index f4c0d9fe8f..c1a3a45445 100644
--- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
+++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
@@ -108,6 +108,49 @@ namespace Jellyfin.Model.Tests.Entities
IsExternal = true
});
+ // Test LocalizedLanguage is used when set (fixes zh-CN display issue #15935)
+ data.Add(
+ "Chinese (Simplified) - SRT",
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = null,
+ Language = "zh-CN",
+ LocalizedLanguage = "Chinese (Simplified)",
+ IsForced = false,
+ IsDefault = false,
+ Codec = "SRT"
+ });
+
+ // Test LocalizedLanguage for audio streams
+ data.Add(
+ "Japanese - AAC - Stereo",
+ new MediaStream
+ {
+ Type = MediaStreamType.Audio,
+ Title = null,
+ Language = "jpn",
+ LocalizedLanguage = "Japanese",
+ IsForced = false,
+ IsDefault = false,
+ Codec = "AAC",
+ ChannelLayout = "stereo"
+ });
+
+ // Test fallback to Language when LocalizedLanguage is null
+ data.Add(
+ "Eng - ASS",
+ new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Title = null,
+ Language = "eng",
+ LocalizedLanguage = null,
+ IsForced = false,
+ IsDefault = false,
+ Codec = "ASS"
+ });
+
return data;
}
diff --git a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs
new file mode 100644
index 0000000000..135a139cdf
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs
@@ -0,0 +1,116 @@
+using System.Linq;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.Providers;
+using Xunit;
+
+namespace Jellyfin.Model.Tests.Extensions;
+
+public class EnumerableExtensionsTests
+{
+ [Fact]
+ public void OrderByLanguageDescending_PreferredLanguageFirst()
+ {
+ var images = new[]
+ {
+ new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 100 },
+ new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 },
+ new RemoteImageInfo { Language = null, CommunityRating = 7.0, VoteCount = 50 },
+ new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 150 },
+ };
+
+ var result = images.OrderByLanguageDescending("de").ToList();
+
+ Assert.Equal("de", result[0].Language);
+ Assert.Equal("en", result[1].Language);
+ Assert.Null(result[2].Language);
+ Assert.Equal("fr", result[3].Language);
+ }
+
+ [Fact]
+ public void OrderByLanguageDescending_EnglishBeforeNoLanguage()
+ {
+ var images = new[]
+ {
+ new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 },
+ new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 },
+ };
+
+ var result = images.OrderByLanguageDescending("de").ToList();
+
+ // English should come before no-language, even with lower rating
+ Assert.Equal("en", result[0].Language);
+ Assert.Null(result[1].Language);
+ }
+
+ [Fact]
+ public void OrderByLanguageDescending_SameLanguageSortedByRatingThenVoteCount()
+ {
+ var images = new[]
+ {
+ new RemoteImageInfo { Language = "de", CommunityRating = 5.0, VoteCount = 100 },
+ new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 50 },
+ new RemoteImageInfo { Language = "de", CommunityRating = 9.0, VoteCount = 200 },
+ };
+
+ var result = images.OrderByLanguageDescending("de").ToList();
+
+ Assert.Equal(200, result[0].VoteCount);
+ Assert.Equal(50, result[1].VoteCount);
+ Assert.Equal(100, result[2].VoteCount);
+ }
+
+ [Fact]
+ public void OrderByLanguageDescending_NullRequestedLanguage_DefaultsToEnglish()
+ {
+ var images = new[]
+ {
+ new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 },
+ new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 10 },
+ };
+
+ var result = images.OrderByLanguageDescending(null!).ToList();
+
+ // With null requested language, English becomes the preferred language (score 4)
+ Assert.Equal("en", result[0].Language);
+ Assert.Equal("fr", result[1].Language);
+ }
+
+ [Fact]
+ public void OrderByLanguageDescending_EnglishRequested_NoDoubleBoost()
+ {
+ // When requested language IS English, "en" gets score 4 (requested match),
+ // no-language gets score 2, others get score 0
+ var images = new[]
+ {
+ new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 },
+ new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 },
+ new RemoteImageInfo { Language = "fr", CommunityRating = 8.0, VoteCount = 300 },
+ };
+
+ var result = images.OrderByLanguageDescending("en").ToList();
+
+ Assert.Equal("en", result[0].Language);
+ Assert.Null(result[1].Language);
+ Assert.Equal("fr", result[2].Language);
+ }
+
+ [Fact]
+ public void OrderByLanguageDescending_FullPriorityOrder()
+ {
+ var images = new[]
+ {
+ new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 },
+ new RemoteImageInfo { Language = null, CommunityRating = 8.0, VoteCount = 400 },
+ new RemoteImageInfo { Language = "en", CommunityRating = 7.0, VoteCount = 300 },
+ new RemoteImageInfo { Language = "de", CommunityRating = 6.0, VoteCount = 200 },
+ };
+
+ var result = images.OrderByLanguageDescending("de").ToList();
+
+ // Expected order: de (requested) > en > no-language > fr (other)
+ Assert.Equal("de", result[0].Language);
+ Assert.Equal("en", result[1].Language);
+ Assert.Null(result[2].Language);
+ Assert.Equal("fr", result[3].Language);
+ }
+}
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json
index 68ce3ea4ab..643ff2638c 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json
@@ -152,7 +152,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -169,7 +168,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -185,7 +183,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json
index 3d3968268f..44f63f384f 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json
@@ -130,7 +130,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -146,7 +145,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json
index 5d1f5f1620..f1fc9e0dbe 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json
@@ -127,7 +127,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -144,7 +143,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -161,7 +159,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -178,7 +175,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -195,7 +191,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -212,7 +207,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -229,7 +223,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -246,7 +239,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -263,7 +255,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -281,7 +272,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -298,7 +288,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json
index e2f75b569b..7e37a6236e 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json
@@ -107,7 +107,6 @@
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
- "BreakOnNonKeyFrames": true,
"EnableAudioVbrEncoding": true
},
{
@@ -182,8 +181,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
- "MinSegments": "2",
- "BreakOnNonKeyFrames": true
+ "MinSegments": "2"
},
{
"Container": "ts",
@@ -193,8 +191,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
- "MinSegments": "2",
- "BreakOnNonKeyFrames": true
+ "MinSegments": "2"
}
],
"ContainerProfiles": [],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json
index 21ae7e5cb3..4380d80efa 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json
@@ -95,7 +95,6 @@
"TranscodingProfiles": [
{
"AudioCodec": "aac",
- "BreakOnNonKeyFrames": true,
"Container": "mp4",
"Context": "Streaming",
"EnableAudioVbrEncoding": true,
@@ -170,7 +169,6 @@
},
{
"AudioCodec": "aac,mp2,opus,flac",
- "BreakOnNonKeyFrames": true,
"Container": "mp4",
"Context": "Streaming",
"MaxAudioChannels": "2",
@@ -181,7 +179,6 @@
},
{
"AudioCodec": "aac,mp3,mp2",
- "BreakOnNonKeyFrames": true,
"Container": "ts",
"Context": "Streaming",
"MaxAudioChannels": "2",
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json
index da9a1a4ada..cca1c16ee7 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json
@@ -30,7 +30,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -48,7 +47,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -62,7 +60,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json
index 82b73fb0f8..b7cd170b9f 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json
@@ -30,7 +30,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -48,7 +47,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -62,7 +60,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json
index 37b923558b..b823ac4b85 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json
@@ -49,7 +49,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -66,7 +65,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -83,7 +81,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -100,7 +97,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -118,7 +114,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 1,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -135,7 +130,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json
index 542bf6370a..708ff73c4d 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json
@@ -49,7 +49,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -66,7 +65,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -83,7 +81,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -100,7 +97,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -118,7 +114,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 1,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -135,7 +130,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json
index f61d0e36bd..10382fa82e 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json
@@ -114,7 +114,6 @@
"Protocol": "hls",
"MaxAudioChannels": "6",
"MinSegments": "2",
- "BreakOnNonKeyFrames": true,
"EnableAudioVbrEncoding": true
},
{
@@ -173,8 +172,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
- "MinSegments": "2",
- "BreakOnNonKeyFrames": true
+ "MinSegments": "2"
},
{
"Container": "ts",
@@ -184,8 +182,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
- "MinSegments": "2",
- "BreakOnNonKeyFrames": true
+ "MinSegments": "2"
}
],
"ContainerProfiles": [],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
index 9d43d2166d..3625b099c1 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json
@@ -165,7 +165,6 @@
"MaxAudioChannels": "2",
"MinSegments": 1,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -182,7 +181,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -199,7 +197,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -216,7 +213,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -233,7 +229,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -250,7 +245,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -267,7 +261,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -284,7 +277,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -301,7 +293,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -319,7 +310,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -346,7 +336,6 @@
"MaxAudioChannels": "2",
"MinSegments": 1,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -373,7 +362,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -399,7 +387,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
index 3859ef9941..deee650b29 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json
@@ -165,7 +165,6 @@
"MaxAudioChannels": "6",
"MinSegments": 1,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -182,7 +181,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -199,7 +197,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -216,7 +213,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -233,7 +229,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -250,7 +245,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -267,7 +261,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -284,7 +277,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -301,7 +293,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -319,7 +310,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -346,7 +336,6 @@
"MaxAudioChannels": "6",
"MinSegments": 1,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -373,7 +362,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -399,7 +387,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json
index 9fc1ae6bb2..38de51b045 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json
@@ -16,7 +16,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -28,7 +27,6 @@
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
- "BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -40,7 +38,6 @@
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
- "BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -64,7 +61,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json
index 094b0723b1..3ff11a684f 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json
@@ -135,7 +135,6 @@
"Protocol": "hls",
"MaxAudioChannels": "6",
"MinSegments": "1",
- "BreakOnNonKeyFrames": false,
"EnableAudioVbrEncoding": true
},
{
@@ -210,8 +209,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "6",
- "MinSegments": "1",
- "BreakOnNonKeyFrames": false
+ "MinSegments": "1"
}
],
"ContainerProfiles": [],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json
index 256c8dc2f0..838a1f920c 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json
@@ -52,7 +52,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -70,7 +69,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -88,7 +86,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json
index 256c8dc2f0..838a1f920c 100644
--- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json
+++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json
@@ -52,7 +52,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -70,7 +69,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -88,7 +86,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
- "BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
index 84758c9c36..b81b7934cd 100644
--- a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs
@@ -19,6 +19,7 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("/some/path/The Show", "The Show")]
[InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")]
[InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")]
+ [InlineData("/some/path/1923 (2022)", "1923")]
public void SeriesResolverResolveTest(string path, string name)
{
var res = SeriesResolver.Resolve(_namingOptions, path);
diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
index 6c9c98cbe8..df5819d747 100644
--- a/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs
@@ -29,6 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("[OCN] 애타는 로맨스 720p-NEXT", "애타는 로맨스")]
[InlineData("[tvN] 혼술남녀.E01-E16.720p-NEXT", "혼술남녀")]
[InlineData("[tvN] 연애말고 결혼 E01~E16 END HDTV.H264.720p-WITH", "연애말고 결혼")]
+ [InlineData("2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE[字].mp4", "2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE")]
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
{
@@ -44,6 +45,7 @@ namespace Jellyfin.Naming.Tests.Video
[InlineData("American.Psycho.mkv")]
[InlineData("American Psycho.mkv")]
[InlineData("Run lola run (lola rennt) (2009).mp4")]
+ [InlineData("2026年01月05日00時55分00秒-[新]違国日記【ANiMiDNiGHT!!!】#1.mp4")]
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
{
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 6b13986957..2fb45600b1 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
@@ -269,8 +270,13 @@ namespace Jellyfin.Naming.Tests.Video
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
_namingOptions).ToList();
- Assert.Equal(7, result.Count);
- Assert.Empty(result[0].AlternateVersions);
+ Assert.Single(result);
+ Assert.Equal(6, result[0].AlternateVersions.Count);
+
+ // Verify 3D recognition is preserved on alternate versions
+ var hsbs = result[0].AlternateVersions.First(v => v.Path.Contains("3d-hsbs", StringComparison.Ordinal));
+ Assert.True(hsbs.Is3D);
+ Assert.Equal("hsbs", hsbs.Format3D);
}
[Fact]
@@ -435,5 +441,39 @@ namespace Jellyfin.Naming.Tests.Video
Assert.Empty(result);
}
+
+ [Fact]
+ public void Resolve_GivenUnderscoreSeparator_GroupsVersions()
+ {
+ var files = new[]
+ {
+ "/movies/Movie (2020)/Movie (2020)_4K.mkv",
+ "/movies/Movie (2020)/Movie (2020)_1080p.mkv"
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ _namingOptions).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ }
+
+ [Fact]
+ public void Resolve_GivenDotSeparator_GroupsVersions()
+ {
+ var files = new[]
+ {
+ "/movies/Movie (2020)/Movie (2020).UHD.mkv",
+ "/movies/Movie (2020)/Movie (2020).1080p.mkv"
+ };
+
+ var result = VideoListResolver.Resolve(
+ files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
+ _namingOptions).ToList();
+
+ Assert.Single(result);
+ Assert.Single(result[0].AlternateVersions);
+ }
}
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 38208476f8..b63009d6a5 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -113,7 +113,7 @@ namespace Jellyfin.Networking.Tests
public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress)
{
var ipa = IPAddress.Parse(ipAddress);
- Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress)));
}
/// <summary>
@@ -131,7 +131,7 @@ namespace Jellyfin.Networking.Tests
public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress)
{
var ipa = IPAddress.Parse(ipAddress);
- Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress)));
}
/// <summary>
@@ -147,7 +147,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")]
public void IPv6SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress)
{
- Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress)));
}
[Theory]
@@ -158,7 +158,7 @@ namespace Jellyfin.Networking.Tests
[InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")]
public void IPv6SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress)
{
- Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress)));
+ Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress)));
}
[Theory]
@@ -377,6 +377,8 @@ namespace Jellyfin.Networking.Tests
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "10.0.0.1", "192.168.1.209", "10.0.0.1")] // LAN not bound, so return external.
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "8.8.8.8", "10.0.0.1")] // return external bind address
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "192.168.1.210", "192.168.1.208")] // return LAN bind address
+ // Cross-subnet IPv4 request should return IPv4, not IPv6 (Issue #15898)
+ [InlineData("192.168.1.208/24,-16,eth16|fd00::1/64,10,eth7", "192.168.1.0/24", "", "192.168.2.100", "192.168.1.208")]
public void GetBindInterface_ValidSourceGiven_Success(string interfaces, string lan, string bind, string source, string result)
{
var conf = new NetworkConfiguration
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs
new file mode 100644
index 0000000000..052bdf740e
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs
@@ -0,0 +1,102 @@
+using System;
+using Emby.Server.Implementations.Cryptography;
+using MediaBrowser.Model.Cryptography;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Cryptography;
+
+public class CryptographyProviderTests
+{
+ private readonly CryptographyProvider _sut = new();
+
+ [Fact]
+ public void CreatePasswordHash_WithPassword_ReturnsHashWithIterations()
+ {
+ var hash = _sut.CreatePasswordHash("testpassword");
+
+ Assert.Equal("PBKDF2-SHA512", hash.Id);
+ Assert.True(hash.Parameters.ContainsKey("iterations"));
+ Assert.NotEmpty(hash.Salt.ToArray());
+ Assert.NotEmpty(hash.Hash.ToArray());
+ }
+
+ [Fact]
+ public void Verify_WithValidPassword_ReturnsTrue()
+ {
+ const string password = "testpassword";
+ var hash = _sut.CreatePasswordHash(password);
+
+ Assert.True(_sut.Verify(hash, password));
+ }
+
+ [Fact]
+ public void Verify_WithWrongPassword_ReturnsFalse()
+ {
+ var hash = _sut.CreatePasswordHash("correctpassword");
+
+ Assert.False(_sut.Verify(hash, "wrongpassword"));
+ }
+
+ [Fact]
+ public void Verify_PBKDF2_MissingIterations_ThrowsFormatException()
+ {
+ var hash = PasswordHash.Parse("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
+
+ var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
+ Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Verify_PBKDF2SHA512_MissingIterations_ThrowsFormatException()
+ {
+ var hash = PasswordHash.Parse("$PBKDF2-SHA512$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
+
+ var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
+ Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Verify_PBKDF2_InvalidIterationsFormat_ThrowsFormatException()
+ {
+ var hash = PasswordHash.Parse("$PBKDF2$iterations=abc$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
+
+ var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
+ Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Verify_PBKDF2SHA512_InvalidIterationsFormat_ThrowsFormatException()
+ {
+ var hash = PasswordHash.Parse("$PBKDF2-SHA512$iterations=notanumber$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
+
+ var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
+ Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Verify_UnsupportedHashId_ThrowsNotSupportedException()
+ {
+ var hash = PasswordHash.Parse("$UNKNOWN$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
+
+ Assert.Throws<NotSupportedException>(() => _sut.Verify(hash, "password"));
+ }
+
+ [Fact]
+ public void GenerateSalt_ReturnsNonEmptyArray()
+ {
+ var salt = _sut.GenerateSalt();
+
+ Assert.NotEmpty(salt);
+ }
+
+ [Theory]
+ [InlineData(16)]
+ [InlineData(32)]
+ [InlineData(64)]
+ public void GenerateSalt_WithLength_ReturnsArrayOfSpecifiedLength(int length)
+ {
+ var salt = _sut.GenerateSalt(length);
+
+ Assert.Equal(length, salt.Length);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs
new file mode 100644
index 0000000000..8fbccd8019
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs
@@ -0,0 +1,109 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Jellyfin.Server.Implementations.Item;
+using MediaBrowser.Controller.Entities.TV;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Data
+{
+ public class SearchPunctuationTests
+ {
+ private readonly IFixture _fixture;
+ private readonly BaseItemRepository _repo;
+
+ public SearchPunctuationTests()
+ {
+ var appHost = new Mock<MediaBrowser.Controller.IServerApplicationHost>();
+ appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>()))
+ .Returns((string x) => x);
+ appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
+ .Returns((string x) => x);
+
+ var configSection = new Mock<IConfigurationSection>();
+ configSection
+ .SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
+ .Returns("0");
+ var config = new Mock<IConfiguration>();
+ config
+ .Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
+ .Returns(configSection.Object);
+
+ _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ _fixture.Inject(appHost.Object);
+ _fixture.Inject(config.Object);
+
+ _repo = _fixture.Create<BaseItemRepository>();
+ }
+
+ [Fact]
+ public void CleanName_keeps_punctuation_and_search_without_punctuation_passes()
+ {
+ var series = new Series
+ {
+ Id = Guid.NewGuid(),
+ Name = "Mr. Robot"
+ };
+
+ series.SortName = "Mr. Robot";
+
+ var entity = _repo.Map(series);
+ Assert.Equal("mr robot", entity.CleanName);
+
+ var searchTerm = "Mr Robot".ToLowerInvariant();
+
+ Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory]
+ [InlineData("Spider-Man: Homecoming", "spider man homecoming")]
+ [InlineData("Beyoncé — Live!", "beyonce live")]
+ [InlineData("Hello, World!", "hello world")]
+ [InlineData("(The) Good, the Bad & the Ugly", "the good the bad the ugly")]
+ [InlineData("Wall-E", "wall e")]
+ [InlineData("No. 1: The Beginning", "no 1 the beginning")]
+ [InlineData("Café-au-lait", "cafe au lait")]
+ public void CleanName_normalizes_various_punctuation(string title, string expectedClean)
+ {
+ var series = new Series
+ {
+ Id = Guid.NewGuid(),
+ Name = title
+ };
+
+ series.SortName = title;
+
+ var entity = _repo.Map(series);
+
+ Assert.Equal(expectedClean, entity.CleanName);
+
+ // Ensure a search term without punctuation would match
+ var searchTerm = expectedClean;
+ Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory]
+ [InlineData("Face/Off", "face off")]
+ [InlineData("V/H/S", "v h s")]
+ public void CleanName_normalizes_titles_withslashes(string title, string expectedClean)
+ {
+ var series = new Series
+ {
+ Id = Guid.NewGuid(),
+ Name = title
+ };
+
+ series.SortName = title;
+
+ var entity = _repo.Map(series);
+
+ Assert.Equal(expectedClean, entity.CleanName);
+
+ // Ensure a search term without punctuation would match
+ var searchTerm = expectedClean;
+ Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs
new file mode 100644
index 0000000000..c450cbb0e1
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs
@@ -0,0 +1,72 @@
+using System;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Server.Implementations.Item;
+using MediaBrowser.Controller;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Item;
+
+public class BaseItemRepositoryTests
+{
+ [Fact]
+ public void DeserializeBaseItem_WithUnknownType_ReturnsNull()
+ {
+ // Arrange
+ var entity = new BaseItemEntity
+ {
+ Id = Guid.NewGuid(),
+ Type = "NonExistent.Plugin.CustomItemType"
+ };
+
+ // Act
+ var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void DeserializeBaseItem_WithUnknownType_LogsWarning()
+ {
+ // Arrange
+ var entity = new BaseItemEntity
+ {
+ Id = Guid.NewGuid(),
+ Type = "NonExistent.Plugin.CustomItemType"
+ };
+ var loggerMock = new Mock<ILogger>();
+
+ // Act
+ BaseItemRepository.DeserializeBaseItem(entity, loggerMock.Object, null, false);
+
+ // Assert
+ loggerMock.Verify(
+ x => x.Log(
+ LogLevel.Warning,
+ It.IsAny<EventId>(),
+ It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unknown type", StringComparison.OrdinalIgnoreCase)),
+ It.IsAny<Exception?>(),
+ It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
+ Times.Once);
+ }
+
+ [Fact]
+ public void DeserializeBaseItem_WithKnownType_ReturnsItem()
+ {
+ // Arrange
+ var entity = new BaseItemEntity
+ {
+ Id = Guid.NewGuid(),
+ Type = "MediaBrowser.Controller.Entities.Movies.Movie"
+ };
+
+ // Act
+ var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
+
+ // Assert
+ Assert.NotNull(result);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
index d677c9f091..a7bbef7ed4 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs
@@ -1,30 +1,81 @@
+using Emby.Server.Implementations.Library;
using Xunit;
namespace Jellyfin.Server.Implementations.Tests.Library;
public class DotIgnoreIgnoreRuleTest
{
- [Fact]
- public void Test()
+ private static readonly string[] _rule1 = ["SPs"];
+ private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"];
+ private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"];
+ private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"];
+
+ public static TheoryData<string[], string, bool, bool> CheckIgnoreRulesTestData =>
+ new()
+ {
+ // Basic pattern matching
+ { _rule1, "f:/cd/sps/ffffff.mkv", false, true },
+ { _rule1, "cd/sps/ffffff.mkv", false, true },
+ { _rule1, "/cd/sps/ffffff.mkv", false, true },
+
+ // Negate pattern
+ { _rule2, "f:/cd/sps/ffffff.mkv", false, true },
+ { _rule2, "cd/sps/ffffff.mkv", false, true },
+ { _rule2, "/cd/sps/ffffff.mkv", false, true },
+ { _rule2, "f:/cd/sps/thebestshot.mkv", false, false },
+ { _rule2, "cd/sps/thebestshot.mkv", false, false },
+ { _rule2, "/cd/sps/thebestshot.mkv", false, false },
+
+ // Mixed valid and invalid patterns - skips invalid, applies valid
+ { _rule3, "test.txt", false, true },
+ { _rule3, "videos/movie.mp4", false, true },
+ { _rule3, "movie.mkv", false, true },
+ { _rule3, "test.mp3", false, false },
+
+ // Only invalid patterns - falls back to ignore all
+ { _rule4, "any-file.txt", false, true },
+ { _rule4, "any/path/to/file.mkv", false, true },
+ };
+
+ public static TheoryData<string[], string, bool, bool> WindowsPathNormalizationTestData =>
+ new()
+ {
+ // Windows paths with backslashes - should match when normalizePath is true
+ { _rule1, @"C:\cd\sps\ffffff.mkv", false, true },
+ { _rule1, @"D:\media\sps\movie.mkv", false, true },
+ { _rule1, @"\\server\share\sps\file.mkv", false, true },
+
+ // Negate pattern with Windows paths
+ { _rule2, @"C:\cd\sps\ffffff.mkv", false, true },
+ { _rule2, @"C:\cd\sps\thebestshot.mkv", false, false },
+
+ // Directory matching with Windows paths
+ { _rule3, @"C:\videos\movie.mp4", false, true },
+ { _rule3, @"D:\documents\test.txt", false, true },
+ { _rule3, @"E:\music\song.mp3", false, false },
+ };
+
+ [Theory]
+ [MemberData(nameof(CheckIgnoreRulesTestData))]
+ public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored)
+ {
+ Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory));
+ }
+
+ [Theory]
+ [MemberData(nameof(WindowsPathNormalizationTestData))]
+ public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored)
{
- var ignore = new Ignore.Ignore();
- ignore.Add("SPs");
- Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv"));
- Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv"));
- Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv"));
+ // With normalizePath=true, backslashes should be converted to forward slashes
+ Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true));
}
- [Fact]
- public void TestNegatePattern()
+ [Theory]
+ [InlineData(@"C:\cd\sps\ffffff.mkv")]
+ [InlineData(@"D:\media\sps\movie.mkv")]
+ public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path)
{
- var ignore = new Ignore.Ignore();
- ignore.Add("SPs");
- ignore.Add("!thebestshot.mkv");
- Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv"));
- Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv"));
- Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv"));
- Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv"));
- Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv"));
- Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv"));
+ // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes
+ Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false));
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
index 940e3c2b12..650d67b195 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs
@@ -11,21 +11,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
- [InlineData("Superman: Red Son", "something", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
- [InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
- [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")]
- [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
+ [InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
+ [InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
+ [InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
+ [InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
+ [InlineData("{tmdbid=618355}", "tmdbid", "618355")]
+ [InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
+ [InlineData("{tmdbid-618355)", "tmdbid", null)]
+ [InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)]
@@ -36,6 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
+ [InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
+ [InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
+ [InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 6d6bba4fc4..5bcfc580ff 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
});
var countries = localizationManager.GetCountries().ToList();
- Assert.Equal(139, countries.Count);
+ Assert.Equal(140, countries.Count);
var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal));
Assert.NotNull(germany);
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
- Assert.Equal(194, cultures.Count);
+ Assert.Equal(496, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
@@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
}
+ [Theory]
+ [InlineData("mul", "Multiple languages")]
+ [InlineData("und", "Undetermined")]
+ [InlineData("mis", "Uncoded languages")]
+ [InlineData("zxx", "No linguistic content; Not applicable")]
+ public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName)
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "en-US"
+ });
+ await localizationManager.LoadAll();
+
+ var culture = localizationManager.FindLanguageInfo(code);
+ Assert.NotNull(culture);
+ Assert.Equal(expectedDisplayName, culture.DisplayName);
+ Assert.Equal(code, culture.ThreeLetterISOLanguageName);
+ }
+
[Fact]
public async Task GetParentalRatings_Default_Success()
{
@@ -204,6 +223,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
}
[Theory]
+ [InlineData("TV-MA", "DE", 17, 1)] // US-only rating, DE country code
+ [InlineData("PG-13", "FR", 13, 0)] // US-only rating, FR country code
+ [InlineData("R", "JP", 17, 0)] // US-only rating, JP country code
+ public async Task GetRatingScore_FallbackPrioritizesUS_Success(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/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
index 16c63ed49e..04d1b3dc27 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs
@@ -61,7 +61,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions);
Assert.NotNull(users);
Assert.Single(users);
- Assert.False(users![0].HasConfiguredPassword);
}
[Fact]
@@ -92,8 +91,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions);
Assert.Equal(TestUsername, user!.Name);
- Assert.False(user.HasPassword);
- Assert.False(user.HasConfiguredPassword);
_testUserId = user.Id;
@@ -149,12 +146,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
using var response = await UpdateUserPassword(client, _testUserId, createRequest);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
-
- var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await client.GetStreamAsync("Users"), _jsonOptions);
- var user = users!.First(x => x.Id.Equals(_testUserId));
- Assert.True(user.HasPassword);
- Assert.True(user.HasConfiguredPassword);
}
[Fact]
@@ -172,12 +163,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
using var response = await UpdateUserPassword(client, _testUserId, createRequest);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
-
- var users = await JsonSerializer.DeserializeAsync<UserDto[]>(
- await client.GetStreamAsync("Users"), _jsonOptions);
- var user = users!.First(x => x.Id.Equals(_testUserId));
- Assert.False(user.HasPassword);
- Assert.False(user.HasConfiguredPassword);
}
}
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 8228c0df70..7b0e23788b 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -5,7 +5,6 @@
<PackageReference Include="AutoFixture.AutoMoq" />
<PackageReference Include="AutoFixture.Xunit2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
- <PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 5fea805ae1..21596e0ed2 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -5,7 +5,6 @@
<PackageReference Include="AutoFixture.AutoMoq" />
<PackageReference Include="AutoFixture.Xunit2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
- <PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
index 123266d298..14f4c33b6b 100644
--- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
+++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs
@@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager;
-using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
namespace Jellyfin.Server.Tests
{
@@ -87,7 +86,7 @@ namespace Jellyfin.Server.Tests
// Need this here as ::1 and 127.0.0.1 are in them by default.
options.KnownProxies.Clear();
- options.KnownNetworks.Clear();
+ options.KnownIPNetworks.Clear();
ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList, options);
@@ -97,10 +96,10 @@ namespace Jellyfin.Server.Tests
Assert.True(options.KnownProxies.Contains(item));
}
- Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count);
+ Assert.Equal(knownNetworks.Length, options.KnownIPNetworks.Count);
foreach (var item in knownNetworks)
{
- Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.Prefix) && x.PrefixLength == item.PrefixLength));
+ Assert.NotEqual(default, options.KnownIPNetworks.FirstOrDefault(x => x.BaseAddress.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength));
}
}