From 2b6febc8da5af1018a1fa73a873bdc617dc1f084 Mon Sep 17 00:00:00 2001 From: loop Date: Mon, 25 Aug 2025 10:28:20 +0200 Subject: Fix: Add 'Kairon; IRSE!' to artist whitelist --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 997c7d2a48..e908bf6bc5 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -81,6 +81,7 @@ namespace MediaBrowser.MediaEncoding.Probing "Smith/Kotzen", "We;Na", "LSR/CITY", + "Kairon; IRSE!", }; /// -- cgit v1.2.3 From 88acd51ee2b6a30a8a92a9e863fd4aaef48ee590 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 28 Dec 2025 07:22:31 -0500 Subject: Backport pull request #15807 from jellyfin/release-10.11.z Enforce more strict webm check Original-merge: 8379b4634aeaf9827d07a41cf9ba8fd80c8c323e Merged-by: crobibero Backported-by: Bond_009 --- .../Probing/ProbeResultNormalizer.cs | 9 +- .../Probing/ProbeResultNormalizerTests.cs | 12 ++ .../Probing/video_web_like_mkv_with_subtitle.json | 137 +++++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 1823496bf1..570ef9ee27 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -300,9 +300,12 @@ namespace MediaBrowser.MediaEncoding.Probing // Handle WebM else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase)) { - // Limit WebM to supported codecs - if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) - || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) + // Limit WebM to supported stream types and codecs. + // FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible. + // Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result. + if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio) + || mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) + || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) { splitFormat[i] = string.Empty; } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 94710a0957..8a2f84734e 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -195,6 +195,18 @@ namespace Jellyfin.MediaEncoding.Tests.Probing 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(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] public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success() { 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 + } +} -- cgit v1.2.3 From 7af5ee1812a18eb062158618de38b6102c00595f Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 28 Dec 2025 07:22:36 -0500 Subject: Backport pull request #15834 from jellyfin/release-10.11.z Fix video lacking SAR and DAR are marked as anamorphic Original-merge: 9470439cfa1eaf7cb9717f16031b020cedab516a Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 570ef9ee27..50f7716d86 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -857,7 +857,12 @@ namespace MediaBrowser.MediaEncoding.Probing } // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe - if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) + if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio) + && string.IsNullOrEmpty(streamInfo.DisplayAspectRatio)) + { + stream.IsAnamorphic = false; + } + else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) { stream.IsAnamorphic = false; } -- cgit v1.2.3 From bc316b3dc855e93d4d11e2c0d73d70326c38b889 Mon Sep 17 00:00:00 2001 From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:00:05 +0400 Subject: Fix near-1:1 SAR values falsely flagged as anamorphic Encoders sometimes produce sample aspect ratios like 3201:3200 (0.03% off square) for content that has effectively square pixels. The exact string comparison against "1:1" marks these as anamorphic, which triggers unnecessary transcoding on clients that require non-anamorphic video. Parse the SAR ratio numerically and treat values within 1% of 1:1 as square pixels. This threshold is well clear of the nearest real anamorphic SAR (PAL 4:3 at 16:15 = 6.67% off). --- .../Probing/ProbeResultNormalizer.cs | 28 +++++++++++++++++++++- .../Probing/ProbeResultNormalizerTests.cs | 18 ++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index dbe5322897..471df369ba 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -863,7 +863,7 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.IsAnamorphic = false; } - else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) + else if (IsNearSquarePixelSar(streamInfo.SampleAspectRatio)) { stream.IsAnamorphic = false; } @@ -1154,6 +1154,32 @@ namespace MediaBrowser.MediaEncoding.Probing return Math.Abs(d1 - d2) <= variance; } + /// + /// Determines whether a sample aspect ratio represents square (or near-square) pixels. + /// Some encoders produce SARs like 3201:3200 for content that is effectively 1:1, + /// which would be falsely classified as anamorphic by an exact string comparison. + /// A 1% tolerance safely covers encoder rounding artifacts while preserving detection + /// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off). + /// + internal static bool IsNearSquarePixelSar(string sar) + { + if (string.IsNullOrEmpty(sar)) + { + return false; + } + + var parts = sar.Split(':'); + if (parts.Length == 2 + && double.TryParse(parts[0], CultureInfo.InvariantCulture, out var num) + && double.TryParse(parts[1], CultureInfo.InvariantCulture, out var den) + && den > 0) + { + return IsClose(num / den, 1.0, 0.01); + } + + return string.Equals(sar, "1:1", StringComparison.Ordinal); + } + /// /// Gets a frame rate from a string value in ffprobe output /// This could be a number or in the format of 2997/125. diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 8a2f84734e..40f853699b 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)] // exact square pixels + [InlineData("3201:3200", true)] // 0.03% off — encoder rounding artifact (4K HEVC) + [InlineData("1215:1216", true)] // 0.08% off — encoder rounding artifact + [InlineData("1001:1000", true)] // 0.1% off — encoder rounding artifact + [InlineData("16:15", false)] // 6.67% off — PAL DVD 4:3, genuinely anamorphic + [InlineData("8:9", false)] // 11.1% off — NTSC DVD 4:3 + [InlineData("32:27", false)] // 18.5% off — NTSC DVD 16:9 + [InlineData("10:11", false)] // 9.1% off — DV NTSC + [InlineData("64:45", false)] // 42.2% off — PAL DVD 16:9 + [InlineData("4:3", false)] // 33.3% off — classic anamorphic + [InlineData("0:1", false)] // invalid/unknown SAR + [InlineData("", false)] // empty + [InlineData(null, false)] // null + 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); -- cgit v1.2.3 From d87fe973f3516e20ae4c4ecd8036286deeb4b51d Mon Sep 17 00:00:00 2001 From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:51:27 +0400 Subject: Fix StyleCop and xUnit analyzer errors - Add missing param and returns XML doc tags (SA1611, SA1615) - Remove trailing alignment whitespace in test attributes (SA1025) - Use nullable string parameter for null test case (xUnit1012) --- .../Probing/ProbeResultNormalizer.cs | 2 ++ .../Probing/ProbeResultNormalizerTests.cs | 28 +++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 471df369ba..127bdd380d 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1161,6 +1161,8 @@ namespace MediaBrowser.MediaEncoding.Probing /// A 1% tolerance safely covers encoder rounding artifacts while preserving detection /// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off). /// + /// The sample aspect ratio string in "N:D" format. + /// true if the SAR is within 1% of 1:1; otherwise false. internal static bool IsNearSquarePixelSar(string sar) { if (string.IsNullOrEmpty(sar)) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 40f853699b..8ebbd029ac 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -40,20 +40,20 @@ namespace Jellyfin.MediaEncoding.Tests.Probing => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); [Theory] - [InlineData("1:1", true)] // exact square pixels - [InlineData("3201:3200", true)] // 0.03% off — encoder rounding artifact (4K HEVC) - [InlineData("1215:1216", true)] // 0.08% off — encoder rounding artifact - [InlineData("1001:1000", true)] // 0.1% off — encoder rounding artifact - [InlineData("16:15", false)] // 6.67% off — PAL DVD 4:3, genuinely anamorphic - [InlineData("8:9", false)] // 11.1% off — NTSC DVD 4:3 - [InlineData("32:27", false)] // 18.5% off — NTSC DVD 16:9 - [InlineData("10:11", false)] // 9.1% off — DV NTSC - [InlineData("64:45", false)] // 42.2% off — PAL DVD 16:9 - [InlineData("4:3", false)] // 33.3% off — classic anamorphic - [InlineData("0:1", false)] // invalid/unknown SAR - [InlineData("", false)] // empty - [InlineData(null, false)] // null - public void IsNearSquarePixelSar_DetectsCorrectly(string sar, bool expected) + [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] -- cgit v1.2.3 From a6da575785e678e64ed03978d1f4f60a80423121 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 29 Mar 2026 14:16:26 +0200 Subject: Only set IsAvc for video streams Also enables nullable for MediaStreamInfo Makes more properties nullable that aren't always present --- .../Probing/FFProbeHelpers.cs | 4 +- .../Probing/MediaStreamInfo.cs | 82 +++++++++++----------- .../Probing/ProbeResultNormalizer.cs | 23 ++---- MediaBrowser.Model/Entities/MediaStream.cs | 1 - .../Probing/ProbeResultNormalizerTests.cs | 4 +- 5 files changed, 50 insertions(+), 64 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs index 6f51e1a6ab..975c2b8161 100644 --- a/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs +++ b/MediaBrowser.MediaEncoding/Probing/FFProbeHelpers.cs @@ -74,9 +74,9 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The dict. /// Dictionary{System.StringSystem.String}. - private static Dictionary ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary dict) + private static Dictionary ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary dict) { - return new Dictionary(dict, StringComparer.OrdinalIgnoreCase); + return new Dictionary(dict, StringComparer.OrdinalIgnoreCase); } } } diff --git a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs index 2944423248..f631c471f6 100644 --- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs +++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - using System.Collections.Generic; using System.Text.Json.Serialization; @@ -22,21 +20,21 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The profile. [JsonPropertyName("profile")] - public string Profile { get; set; } + public string? Profile { get; set; } /// /// Gets or sets the codec_name. /// /// The codec_name. [JsonPropertyName("codec_name")] - public string CodecName { get; set; } + public string? CodecName { get; set; } /// /// Gets or sets the codec_long_name. /// /// The codec_long_name. [JsonPropertyName("codec_long_name")] - public string CodecLongName { get; set; } + public string? CodecLongName { get; set; } /// /// Gets or sets the codec_type. @@ -50,49 +48,49 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The sample_rate. [JsonPropertyName("sample_rate")] - public string SampleRate { get; set; } + public string? SampleRate { get; set; } /// /// Gets or sets the channels. /// /// The channels. [JsonPropertyName("channels")] - public int Channels { get; set; } + public int? Channels { get; set; } /// /// Gets or sets the channel_layout. /// /// The channel_layout. [JsonPropertyName("channel_layout")] - public string ChannelLayout { get; set; } + public string? ChannelLayout { get; set; } /// /// Gets or sets the avg_frame_rate. /// /// The avg_frame_rate. [JsonPropertyName("avg_frame_rate")] - public string AverageFrameRate { get; set; } + public string? AverageFrameRate { get; set; } /// /// Gets or sets the duration. /// /// The duration. [JsonPropertyName("duration")] - public string Duration { get; set; } + public string? Duration { get; set; } /// /// Gets or sets the bit_rate. /// /// The bit_rate. [JsonPropertyName("bit_rate")] - public string BitRate { get; set; } + public string? BitRate { get; set; } /// /// Gets or sets the width. /// /// The width. [JsonPropertyName("width")] - public int Width { get; set; } + public int? Width { get; set; } /// /// Gets or sets the refs. @@ -106,21 +104,21 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The height. [JsonPropertyName("height")] - public int Height { get; set; } + public int? Height { get; set; } /// /// Gets or sets the display_aspect_ratio. /// /// The display_aspect_ratio. [JsonPropertyName("display_aspect_ratio")] - public string DisplayAspectRatio { get; set; } + public string? DisplayAspectRatio { get; set; } /// /// Gets or sets the tags. /// /// The tags. [JsonPropertyName("tags")] - public IReadOnlyDictionary Tags { get; set; } + public IReadOnlyDictionary? Tags { get; set; } /// /// Gets or sets the bits_per_sample. @@ -141,7 +139,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The r_frame_rate. [JsonPropertyName("r_frame_rate")] - public string RFrameRate { get; set; } + public string? RFrameRate { get; set; } /// /// Gets or sets the has_b_frames. @@ -155,70 +153,70 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The sample_aspect_ratio. [JsonPropertyName("sample_aspect_ratio")] - public string SampleAspectRatio { get; set; } + public string? SampleAspectRatio { get; set; } /// /// Gets or sets the pix_fmt. /// /// The pix_fmt. [JsonPropertyName("pix_fmt")] - public string PixelFormat { get; set; } + public string? PixelFormat { get; set; } /// /// Gets or sets the level. /// /// The level. [JsonPropertyName("level")] - public int Level { get; set; } + public int? Level { get; set; } /// /// Gets or sets the time_base. /// /// The time_base. [JsonPropertyName("time_base")] - public string TimeBase { get; set; } + public string? TimeBase { get; set; } /// /// Gets or sets the start_time. /// /// The start_time. [JsonPropertyName("start_time")] - public string StartTime { get; set; } + public string? StartTime { get; set; } /// /// Gets or sets the codec_time_base. /// /// The codec_time_base. [JsonPropertyName("codec_time_base")] - public string CodecTimeBase { get; set; } + public string? CodecTimeBase { get; set; } /// /// Gets or sets the codec_tag. /// /// The codec_tag. [JsonPropertyName("codec_tag")] - public string CodecTag { get; set; } + public string? CodecTag { get; set; } /// - /// Gets or sets the codec_tag_string. + /// Gets or sets the codec_tag_string?. /// - /// The codec_tag_string. - [JsonPropertyName("codec_tag_string")] - public string CodecTagString { get; set; } + /// The codec_tag_string?. + [JsonPropertyName("codec_tag_string?")] + public string? CodecTagString { get; set; } /// /// Gets or sets the sample_fmt. /// /// The sample_fmt. [JsonPropertyName("sample_fmt")] - public string SampleFmt { get; set; } + public string? SampleFmt { get; set; } /// /// Gets or sets the dmix_mode. /// /// The dmix_mode. [JsonPropertyName("dmix_mode")] - public string DmixMode { get; set; } + public string? DmixMode { get; set; } /// /// Gets or sets the start_pts. @@ -232,90 +230,90 @@ namespace MediaBrowser.MediaEncoding.Probing /// /// The is_avc. [JsonPropertyName("is_avc")] - public bool IsAvc { get; set; } + public bool? IsAvc { get; set; } /// /// Gets or sets the nal_length_size. /// /// The nal_length_size. [JsonPropertyName("nal_length_size")] - public string NalLengthSize { get; set; } + public string? NalLengthSize { get; set; } /// /// Gets or sets the ltrt_cmixlev. /// /// The ltrt_cmixlev. [JsonPropertyName("ltrt_cmixlev")] - public string LtrtCmixlev { get; set; } + public string? LtrtCmixlev { get; set; } /// /// Gets or sets the ltrt_surmixlev. /// /// The ltrt_surmixlev. [JsonPropertyName("ltrt_surmixlev")] - public string LtrtSurmixlev { get; set; } + public string? LtrtSurmixlev { get; set; } /// /// Gets or sets the loro_cmixlev. /// /// The loro_cmixlev. [JsonPropertyName("loro_cmixlev")] - public string LoroCmixlev { get; set; } + public string? LoroCmixlev { get; set; } /// /// Gets or sets the loro_surmixlev. /// /// The loro_surmixlev. [JsonPropertyName("loro_surmixlev")] - public string LoroSurmixlev { get; set; } + public string? LoroSurmixlev { get; set; } /// /// Gets or sets the field_order. /// /// The field_order. [JsonPropertyName("field_order")] - public string FieldOrder { get; set; } + public string? FieldOrder { get; set; } /// /// Gets or sets the disposition. /// /// The disposition. [JsonPropertyName("disposition")] - public IReadOnlyDictionary Disposition { get; set; } + public IReadOnlyDictionary? Disposition { get; set; } /// /// Gets or sets the color range. /// /// The color range. [JsonPropertyName("color_range")] - public string ColorRange { get; set; } + public string? ColorRange { get; set; } /// /// Gets or sets the color space. /// /// The color space. [JsonPropertyName("color_space")] - public string ColorSpace { get; set; } + public string? ColorSpace { get; set; } /// /// Gets or sets the color transfer. /// /// The color transfer. [JsonPropertyName("color_transfer")] - public string ColorTransfer { get; set; } + public string? ColorTransfer { get; set; } /// /// Gets or sets the color primaries. /// /// The color primaries. [JsonPropertyName("color_primaries")] - public string ColorPrimaries { get; set; } + public string? ColorPrimaries { get; set; } /// /// Gets or sets the side_data_list. /// /// The side_data_list. [JsonPropertyName("side_data_list")] - public IReadOnlyList SideDataList { get; set; } + public IReadOnlyList? SideDataList { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 127bdd380d..d3e7b52315 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -697,24 +697,18 @@ namespace MediaBrowser.MediaEncoding.Probing /// MediaStream. private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList frameInfoList) { - // These are mp4 chapters - if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase)) - { - // Edit: but these are also sometimes subtitles? - // return null; - } - var stream = new MediaStream { Codec = streamInfo.CodecName, Profile = streamInfo.Profile, + Width = streamInfo.Width, + Height = streamInfo.Height, Level = streamInfo.Level, Index = streamInfo.Index, PixelFormat = streamInfo.PixelFormat, NalLengthSize = streamInfo.NalLengthSize, TimeBase = streamInfo.TimeBase, - CodecTimeBase = streamInfo.CodecTimeBase, - IsAVC = streamInfo.IsAvc + CodecTimeBase = streamInfo.CodecTimeBase }; // Filter out junk @@ -774,10 +768,6 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); - // Graphical subtitle may have width and height info - stream.Width = streamInfo.Width; - stream.Height = streamInfo.Height; - if (string.IsNullOrEmpty(stream.Title)) { // mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler" @@ -790,6 +780,7 @@ namespace MediaBrowser.MediaEncoding.Probing } else if (streamInfo.CodecType == CodecType.Video) { + stream.IsAVC = streamInfo.IsAvc; stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate); stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate); @@ -822,8 +813,6 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Video; } - stream.Width = streamInfo.Width; - stream.Height = streamInfo.Height; stream.AspectRatio = GetAspectRatio(streamInfo); if (streamInfo.BitsPerSample > 0) @@ -1091,8 +1080,8 @@ namespace MediaBrowser.MediaEncoding.Probing && width > 0 && height > 0)) { - width = info.Width; - height = info.Height; + width = info.Width.Value; + height = info.Height.Value; } if (width > 0 && height > 0) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index c443af32cf..11f81ff7d8 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 8ebbd029ac..3369af0e84 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -209,8 +209,8 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal("mkv,webm", res.Container); Assert.Equal(2, res.MediaStreams.Count); - - Assert.False(res.MediaStreams[0].IsAVC); + Assert.Equal(540, res.MediaStreams[0].Width); + Assert.Equal(360, res.MediaStreams[0].Height); } [Fact] -- cgit v1.2.3 From 553f38a2377cf843404cd4d3b3602e8a72bc75f8 Mon Sep 17 00:00:00 2001 From: Lasath Fernando Date: Sat, 4 Apr 2026 16:10:07 +0000 Subject: Fix language display for ISO 639-2-only codes (e.g. mul, und) LoadCultures() in LocalizationManager skipped all iso6392.txt entries without a two-letter ISO 639-1 code, dropping 302 of 496 languages including mul (Multiple languages), und (Undetermined), mis (Uncoded languages), zxx, and many real languages like Achinese, Akkadian, etc. This caused FindLanguageInfo() to return null for these codes, which meant: - ExternalPathParser could not recognize them as valid language codes in subtitle filenames, so the Language field was never set - DisplayTitle fell back to the raw code string (e.g. "Mul") Fix by allowing entries without two-letter codes to be loaded with an empty TwoLetterISOLanguageName. Also set LocalizedLanguage in ProbeResultNormalizer for ffprobe-detected streams (the DB repository path was already handled on master). --- .../Localization/LocalizationManager.cs | 2 +- .../Probing/ProbeResultNormalizer.cs | 6 ++++++ MediaBrowser.Model/Entities/MediaStream.cs | 1 - .../Localization/LocalizationManagerTests.cs | 21 ++++++++++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index bc80c2b405..6fca5bc1ba 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization string twoCharName = parts[2]; if (string.IsNullOrWhiteSpace(twoCharName)) { - continue; + twoCharName = string.Empty; } else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index d3e7b52315..203e72de36 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,6 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); + stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) + ? _localization.FindLanguageInfo(stream.Language)?.DisplayName + : null; stream.Channels = streamInfo.Channels; @@ -767,6 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); + stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) + ? _localization.FindLanguageInfo(stream.Language)?.DisplayName + : null; if (string.IsNullOrEmpty(stream.Title)) { diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 11f81ff7d8..4491fb5ace 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Text; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index e60522bf78..700ac5dced 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -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() { -- cgit v1.2.3 From c300651d0ddae2e2a3b3cde55989284f5972a79a Mon Sep 17 00:00:00 2001 From: Lasath Fernando Date: Thu, 9 Apr 2026 13:53:38 -0500 Subject: Simplify null-check ternary style in ProbeResultNormalizer Co-authored-by: Bond-009 --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs') diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 203e72de36..3c6a03713f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,9 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); - stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) - ? _localization.FindLanguageInfo(stream.Language)?.DisplayName - : null; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; stream.Channels = streamInfo.Channels; @@ -770,9 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); - stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) - ? _localization.FindLanguageInfo(stream.Language)?.DisplayName - : null; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; if (string.IsNullOrEmpty(stream.Title)) { -- cgit v1.2.3