From 93902fc610a9d8b52780d88f7bb986e668567c9d Mon Sep 17 00:00:00 2001 From: john janzen Date: Sat, 20 Dec 2025 19:42:51 +0100 Subject: [PATCH 01/94] fix crashes on devices that don't support ipv6 --- src/Jellyfin.Networking/Manager/NetworkManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 126d9f15cf..e82e854417 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -779,6 +779,9 @@ public class NetworkManager : INetworkManager, IDisposable return knownInterfaces; } + // TODO: remove when upgrade to dotnet 11 is done + readIpv6 &= Socket.OSSupportsIPv6; + // No bind address and no exclusions, so listen on all interfaces. var result = new List(); if (readIpv4 && readIpv6) From 146681f0ba927b6c2d1e392a2b157a28c36e1a6b Mon Sep 17 00:00:00 2001 From: john janzen Date: Sun, 21 Dec 2025 15:37:22 +0100 Subject: [PATCH 02/94] Warn server administrator when IPv6 is enabled but unsupported by OS --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 2 +- src/Jellyfin.Networking/Manager/NetworkManager.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 4340969a30..1aa39f97b6 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -162,7 +162,7 @@ public sealed class SetupServer : IDisposable { var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger(), config.EnableIPv4, config.EnableIPv6); knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6); - var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6); + var bindInterfaces = NetworkManager.GetAllBindInterfaces(_loggerFactory.CreateLogger(), false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6); Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer( bindInterfaces, config.InternalHttpPort, diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index e82e854417..88f16d8c50 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -753,12 +753,13 @@ public class NetworkManager : INetworkManager, IDisposable /// public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { - return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); + return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); } /// /// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound. /// + /// Logger to use for messages. /// Defines that only known interfaces should be used. /// The ConfigurationManager. /// The known interfaces that gets returned if possible or instructed. @@ -766,6 +767,7 @@ public class NetworkManager : INetworkManager, IDisposable /// Include IPV6 type interfaces. /// A list of ip address of which jellyfin should bind to. public static IReadOnlyList GetAllBindInterfaces( + ILogger logger, bool individualInterfaces, IConfigurationManager configurationManager, IReadOnlyList knownInterfaces, @@ -780,7 +782,11 @@ public class NetworkManager : INetworkManager, IDisposable } // TODO: remove when upgrade to dotnet 11 is done - readIpv6 &= Socket.OSSupportsIPv6; + if (readIpv6 && !Socket.OSSupportsIPv6) + { + logger.LogWarning("IPv6 Unsupported by OS, not listening on IPv6"); + readIpv6 = false; + } // No bind address and no exclusions, so listen on all interfaces. var result = new List(); From 72b4faa00b743dc5bbd2e25c54b216510e978a5a Mon Sep 17 00:00:00 2001 From: ZeusCraft10 Date: Tue, 30 Dec 2025 17:31:40 -0500 Subject: [PATCH 03/94] Fix UDP Auto-Discovery returning IPv6 for cross-subnet IPv4 requests Fixes #15898 When a UDP discovery request is relayed from a different IPv4 subnet, GetBindAddress() now correctly returns an IPv4 address instead of incorrectly falling back to ::1. Changes: - Loopback fallback now prefers address family matching the source IP - Interface fallback now prefers interfaces matching source address family - Added test case for cross-subnet IPv4 request scenario --- CONTRIBUTORS.md | 1 + .../Manager/NetworkManager.cs | 29 +++++++++++++++++-- .../NetworkParseTests.cs | 2 ++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0fd509f842..9b716a3862 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -164,6 +164,7 @@ - [XVicarious](https://github.com/XVicarious) - [YouKnowBlom](https://github.com/YouKnowBlom) - [ZachPhelan](https://github.com/ZachPhelan) + - [ZeusCraft10](https://github.com/ZeusCraft10) - [KristupasSavickas](https://github.com/KristupasSavickas) - [Pusta](https://github.com/pusta) - [nielsvanvelzen](https://github.com/nielsvanvelzen) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 126d9f15cf..10986b358f 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -875,7 +875,20 @@ public class NetworkManager : INetworkManager, IDisposable if (availableInterfaces.Count == 0) { // There isn't any others, so we'll use the loopback. - result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1"; + // Prefer loopback address matching the source's address family + if (source is not null && source.AddressFamily == AddressFamily.InterNetwork && IsIPv4Enabled) + { + result = "127.0.0.1"; + } + else if (source is not null && source.AddressFamily == AddressFamily.InterNetworkV6 && IsIPv6Enabled) + { + result = "::1"; + } + else + { + result = IsIPv4Enabled ? "127.0.0.1" : "::1"; + } + _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result); return result; } @@ -900,9 +913,19 @@ public class NetworkManager : INetworkManager, IDisposable } } - // Fallback to first available interface + // Fallback to an interface matching the source's address family, or first available + var preferredInterface = availableInterfaces + .FirstOrDefault(x => x.Address.AddressFamily == source.AddressFamily); + + if (preferredInterface is not null) + { + result = NetworkUtils.FormatIPString(preferredInterface.Address); + _logger.LogDebug("{Source}: No matching subnet found, using interface with matching address family: {Result}", source, result); + return result; + } + result = NetworkUtils.FormatIPString(availableInterfaces[0].Address); - _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); + _logger.LogDebug("{Source}: No matching interfaces found, using first available interface as bind address: {Result}", source, result); return result; } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 38208476f8..d8748aadac 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -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 From 0413a8b6d29f0718e7ac9c97b8658c5aedf66e4e Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 21 Nov 2025 09:58:34 +0900 Subject: [PATCH 04/94] include external IDs and URLs for book providers --- .../Books/Isbn/ISBNExternalId.cs | 23 +++++++++++++++ .../Books/Isbn/ISBNExternalUrlProvider.cs | 25 +++++++++++++++++ .../Plugins/ComicVine/ComicVineExternalId.cs | 23 +++++++++++++++ .../ComicVine/ComicVineExternalUrlProvider.cs | 28 +++++++++++++++++++ .../ComicVine/ComicVinePersonExternalId.cs | 23 +++++++++++++++ .../GoogleBooks/GoogleBooksExternalId.cs | 23 +++++++++++++++ .../GoogleBooksExternalUrlProvider.cs | 25 +++++++++++++++++ 7 files changed, 170 insertions(+) create mode 100644 MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs create mode 100644 MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs create mode 100644 MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs create mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs create mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs diff --git a/MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs b/MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs new file mode 100644 index 0000000000..a86275d5ae --- /dev/null +++ b/MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Books.Isbn +{ + /// + public class IsbnExternalId : IExternalId + { + /// + public string ProviderName => "ISBN"; + + /// + public string Key => "ISBN"; + + /// + public ExternalIdMediaType? Type => null; + + /// + public bool Supports(IHasProviderIds item) => item is Book; + } +} diff --git a/MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs b/MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs new file mode 100644 index 0000000000..9d7b1ff208 --- /dev/null +++ b/MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Books.Isbn; + +/// +public class IsbnExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "ISBN"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item.TryGetProviderId("ISBN", out var externalId)) + { + if (item is Book) + { + yield return $"https://search.worldcat.org/search?q=bn:{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs new file mode 100644 index 0000000000..8cbd1f89a7 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.ComicVine +{ + /// + public class ComicVineExternalId : IExternalId + { + /// + public string ProviderName => "Comic Vine"; + + /// + public string Key => "ComicVine"; + + /// + public ExternalIdMediaType? Type => null; + + /// + public bool Supports(IHasProviderIds item) => item is Book; + } +} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs new file mode 100644 index 0000000000..9122399179 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.ComicVine; + +/// +public class ComicVineExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "Comic Vine"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item.TryGetProviderId("ComicVine", out var externalId)) + { + switch (item) + { + case Person: + case Book: + yield return $"https://comicvine.gamespot.com/{externalId}"; + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs b/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs new file mode 100644 index 0000000000..26b8e11380 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.ComicVine +{ + /// + public class ComicVinePersonExternalId : IExternalId + { + /// + public string ProviderName => "Comic Vine"; + + /// + public string Key => "ComicVine"; + + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Person; + + /// + public bool Supports(IHasProviderIds item) => item is Person; + } +} diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs new file mode 100644 index 0000000000..02d3b36974 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs @@ -0,0 +1,23 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace MediaBrowser.Providers.Plugins.GoogleBooks +{ + /// + public class GoogleBooksExternalId : IExternalId + { + /// + public string ProviderName => "Google Books"; + + /// + public string Key => "GoogleBooks"; + + /// + public ExternalIdMediaType? Type => null; + + /// + public bool Supports(IHasProviderIds item) => item is Book; + } +} diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs new file mode 100644 index 0000000000..95047ee83e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.GoogleBooks; + +/// +public class GoogleBooksExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "Google Books"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item.TryGetProviderId("GoogleBooks", out var externalId)) + { + if (item is Book) + { + yield return $"https://books.google.com/books?id={externalId}"; + } + } + } +} From 1c2f08bc173fca586484ece49326d477622ac0bf Mon Sep 17 00:00:00 2001 From: tyage Date: Thu, 12 Feb 2026 00:23:44 +0900 Subject: [PATCH 05/94] Fix filename truncation when bracketed tags appear mid-filename --- Emby.Naming/Common/NamingOptions.cs | 4 ++-- Emby.Naming/Video/CleanStringParser.cs | 2 +- tests/Jellyfin.Naming.Tests/Video/CleanStringTests.cs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index f61ca7e129..eb1c61c5fa 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -152,8 +152,8 @@ namespace Emby.Naming.Common CleanStrings = [ - @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)", - @"^(?.+?)(\[.*\])", + @"^\s*(?.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS)(?=[ _\,\.\(\)\[\]\-]|$)", + @"^\s*(?.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$", @"^\s*(?.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?.+)", @"^\s*(?.+?)\s+-\s+[0-9]+\s*$", diff --git a/Emby.Naming/Video/CleanStringParser.cs b/Emby.Naming/Video/CleanStringParser.cs index a336f8fbd1..f27f8bc0a4 100644 --- a/Emby.Naming/Video/CleanStringParser.cs +++ b/Emby.Naming/Video/CleanStringParser.cs @@ -44,7 +44,7 @@ namespace Emby.Naming.Video var match = expression.Match(name); if (match.Success && match.Groups.TryGetValue("cleaned", out var cleaned)) { - newName = cleaned.Value; + newName = cleaned.Value.Trim(); return true; } 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)); From 8824f07e1b07e5dbcd0641423dc472b67c268d21 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 4 Mar 2026 20:14:21 +0100 Subject: [PATCH 06/94] Don't spam debug log with items without rating --- MediaBrowser.Controller/Entities/BaseItem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index cb38b61119..80abbc7d4d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1600,7 +1600,6 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(rating)) { - Logger.LogDebug("{0} has no parental rating set.", Name); return !GetBlockUnratedValue(user); } From 382db1da0dabf26d9fec1d809c76bbcec70418d5 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Mar 2026 10:49:42 +0100 Subject: [PATCH 07/94] Cleanup trickplay cache dir on failure --- .../Trickplay/TrickplayManager.cs | 110 ++++++++++-------- 1 file changed, 59 insertions(+), 51 deletions(-) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 4505a377ce..63319831e1 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -399,64 +399,72 @@ public class TrickplayManager : ITrickplayManager var workDir = Path.Combine(_appPaths.TempDirectory, "trickplay_" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(workDir); - var trickplayInfo = new TrickplayInfo + try { - Width = width, - Interval = options.Interval, - TileWidth = options.TileWidth, - TileHeight = options.TileHeight, - ThumbnailCount = images.Count, - // Set during image generation - Height = 0, - Bandwidth = 0 - }; - - /* - * Generate trickplay tiles from sets of thumbnails - */ - var imageOptions = new ImageCollageOptions - { - Width = trickplayInfo.TileWidth, - Height = trickplayInfo.TileHeight - }; - - var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; - var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile); - - for (int i = 0; i < requiredTiles; i++) - { - // Set output/input paths - var tilePath = Path.Combine(workDir, $"{i}.jpg"); - - imageOptions.OutputPath = tilePath; - imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList(); - - // Generate image and use returned height for tiles info - var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); - if (trickplayInfo.Height == 0) + var trickplayInfo = new TrickplayInfo { - trickplayInfo.Height = height; + Width = width, + Interval = options.Interval, + TileWidth = options.TileWidth, + TileHeight = options.TileHeight, + ThumbnailCount = images.Count, + // Set during image generation + Height = 0, + Bandwidth = 0 + }; + + /* + * Generate trickplay tiles from sets of thumbnails + */ + var imageOptions = new ImageCollageOptions + { + Width = trickplayInfo.TileWidth, + Height = trickplayInfo.TileHeight + }; + + var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight; + var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile); + + for (int i = 0; i < requiredTiles; i++) + { + // Set output/input paths + var tilePath = Path.Combine(workDir, $"{i}.jpg"); + + imageOptions.OutputPath = tilePath; + imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList(); + + // Generate image and use returned height for tiles info + var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null); + if (trickplayInfo.Height == 0) + { + trickplayInfo.Height = height; + } + + // Update bitrate + var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m)); + trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); } - // Update bitrate - var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m)); - trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate); + /* + * Move trickplay tiles to output directory + */ + Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); + + // Replace existing tiles if they already exist + if (Directory.Exists(outputDir)) + { + Directory.Delete(outputDir, true); + } + + _fileSystem.MoveDirectory(workDir, outputDir); + + return trickplayInfo; } - - /* - * Move trickplay tiles to output directory - */ - Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName); - - // Replace existing tiles if they already exist - if (Directory.Exists(outputDir)) + catch { - Directory.Delete(outputDir, true); + Directory.Delete(workDir, true); + throw; } - - _fileSystem.MoveDirectory(workDir, outputDir); - - return trickplayInfo; } private bool CanGenerateTrickplay(Video video, int interval) From ebb6949ea75bd2f9953c9e1c7708442fa93197fb Mon Sep 17 00:00:00 2001 From: redinsch Date: Sun, 8 Mar 2026 11:29:54 +0100 Subject: [PATCH 08/94] Fix remote image language priority to prefer English over no-language Previously, images with no language were ranked higher (score 3) than English images (score 2), causing poorly rated languageless images to be selected over well-rated English alternatives for posters and logos. Swap the priority so English is preferred over no-language images. Backdrop images are unaffected as they have their own dedicated sorting. Add unit tests for OrderByLanguageDescending. Fixes #13310 --- .../Extensions/EnumerableExtensions.cs | 8 +- .../Extensions/EnumerableExtensionsTests.cs | 117 ++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index 94f4252295..7c9ee18ca4 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -11,7 +11,7 @@ namespace MediaBrowser.Model.Extensions public static class EnumerableExtensions { /// - /// Orders by requested language in descending order, prioritizing "en" over other non-matches. + /// Orders by requested language in descending order, then "en", then no language, over other non-matches. /// /// The remote image infos. /// The requested language for the images. @@ -28,9 +28,9 @@ namespace MediaBrowser.Model.Extensions { // Image priority ordering: // - Images that match the requested language - // - Images with no language // - TODO: Images that match the original language // - Images in English + // - Images with no language // - Images that don't match the requested language if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase)) @@ -38,12 +38,12 @@ namespace MediaBrowser.Model.Extensions return 4; } - if (string.IsNullOrEmpty(i.Language)) + if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) { return 3; } - if (string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(i.Language)) { return 2; } diff --git a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs new file mode 100644 index 0000000000..3b65a2636b --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs @@ -0,0 +1,117 @@ +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); + } + } +} From 3b293751790c32ee1d773a6332a63ba2f3bcab74 Mon Sep 17 00:00:00 2001 From: redinsch Date: Sun, 8 Mar 2026 12:02:08 +0100 Subject: [PATCH 09/94] Use file-scoped namespace in EnumerableExtensionsTests --- .../Extensions/EnumerableExtensionsTests.cs | 169 +++++++++--------- 1 file changed, 84 insertions(+), 85 deletions(-) diff --git a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs index 3b65a2636b..135a139cdf 100644 --- a/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Extensions/EnumerableExtensionsTests.cs @@ -3,115 +3,114 @@ using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Providers; using Xunit; -namespace Jellyfin.Model.Tests.Extensions +namespace Jellyfin.Model.Tests.Extensions; + +public class EnumerableExtensionsTests { - public class EnumerableExtensionsTests + [Fact] + public void OrderByLanguageDescending_PreferredLanguageFirst() { - [Fact] - public void OrderByLanguageDescending_PreferredLanguageFirst() + var images = new[] { - 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 }, - }; + 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(); + 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); - } + 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() + [Fact] + public void OrderByLanguageDescending_EnglishBeforeNoLanguage() + { + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, - new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, - }; + new RemoteImageInfo { Language = null, CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 3.0, VoteCount = 10 }, + }; - var result = images.OrderByLanguageDescending("de").ToList(); + 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); - } + // 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() + [Fact] + public void OrderByLanguageDescending_SameLanguageSortedByRatingThenVoteCount() + { + var images = new[] { - 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 }, - }; + 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(); + var result = images.OrderByLanguageDescending("de").ToList(); - Assert.Equal(200, result[0].VoteCount); - Assert.Equal(50, result[1].VoteCount); - Assert.Equal(100, result[2].VoteCount); - } + Assert.Equal(200, result[0].VoteCount); + Assert.Equal(50, result[1].VoteCount); + Assert.Equal(100, result[2].VoteCount); + } - [Fact] - public void OrderByLanguageDescending_NullRequestedLanguage_DefaultsToEnglish() + [Fact] + public void OrderByLanguageDescending_NullRequestedLanguage_DefaultsToEnglish() + { + var images = new[] { - var images = new[] - { - new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, - new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 10 }, - }; + new RemoteImageInfo { Language = "fr", CommunityRating = 9.0, VoteCount = 500 }, + new RemoteImageInfo { Language = "en", CommunityRating = 5.0, VoteCount = 10 }, + }; - var result = images.OrderByLanguageDescending(null!).ToList(); + 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); - } + // 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() + [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[] { - // 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 }, - }; + 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(); + var result = images.OrderByLanguageDescending("en").ToList(); - Assert.Equal("en", result[0].Language); - Assert.Null(result[1].Language); - Assert.Equal("fr", result[2].Language); - } + Assert.Equal("en", result[0].Language); + Assert.Null(result[1].Language); + Assert.Equal("fr", result[2].Language); + } - [Fact] - public void OrderByLanguageDescending_FullPriorityOrder() + [Fact] + public void OrderByLanguageDescending_FullPriorityOrder() + { + var images = new[] { - 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 }, - }; + 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(); + 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); - } + // 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); } } From 119b2e3d2ff275401870edb66ffcbb4569a9c678 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 10 Mar 2026 19:04:02 -0400 Subject: [PATCH 10/94] Respect library country code for parental ratings --- .../Sorting/OfficialRatingComparer.cs | 4 ++-- MediaBrowser.Controller/Entities/BaseItem.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index 789af01cc3..c0e453d63d 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -41,8 +41,8 @@ public class OfficialRatingComparer : IBaseItemComparer ArgumentNullException.ThrowIfNull(y); var zeroRating = new ParentalRatingScore(0, 0); - var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating; - var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating; + var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating, x.GetPreferredMetadataCountryCode()) ?? zeroRating; + var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating, y.GetPreferredMetadataCountryCode()) ?? zeroRating; var scoreCompare = ratingX.Score.CompareTo(ratingY.Score); if (scoreCompare is 0) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index cb38b61119..2404ace751 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1604,7 +1604,7 @@ namespace MediaBrowser.Controller.Entities return !GetBlockUnratedValue(user); } - var ratingScore = LocalizationManager.GetRatingScore(rating); + var ratingScore = LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode()); // Could not determine rating level if (ratingScore is null) @@ -1646,7 +1646,7 @@ namespace MediaBrowser.Controller.Entities return null; } - return LocalizationManager.GetRatingScore(rating); + return LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode()); } public List GetInheritedTags() @@ -2609,7 +2609,7 @@ namespace MediaBrowser.Controller.Entities .Select(i => i.OfficialRating) .Where(i => !string.IsNullOrEmpty(i)) .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(rating => (rating, LocalizationManager.GetRatingScore(rating))) + .Select(rating => (rating, LocalizationManager.GetRatingScore(rating, GetPreferredMetadataCountryCode()))) .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score) .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore) .Select(i => i.rating); From e4eba084dd0e852e90c7677f9a8b594ecd3f7669 Mon Sep 17 00:00:00 2001 From: jaxx2104 Date: Wed, 11 Mar 2026 23:57:30 +0900 Subject: [PATCH 11/94] Use generic Enum overloads to resolve CA2263 warnings Replace Enum.Parse(typeof(T), ...) and Enum.GetNames(typeof(T)) with their generic counterparts Enum.Parse() and Enum.GetNames() in MediaBrowser.Model/Dlna for improved type safety. Co-Authored-By: Claude Opus 4.6 --- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 2 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 1b61bfe155..79ee683a2d 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -324,7 +324,7 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } - var expected = (TransportStreamTimestamp)Enum.Parse(typeof(TransportStreamTimestamp), condition.Value, true); + var expected = Enum.Parse(condition.Value, true); switch (condition.Condition) { diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 42cb208d08..75b8c137f7 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -2009,7 +2009,7 @@ namespace MediaBrowser.Model.Dlna } else if (condition.Condition == ProfileConditionType.NotEquals) { - item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames(typeof(VideoRangeType)).Except(values))); + item.SetOption(qualifier, "rangetype", string.Join(',', Enum.GetNames().Except(values))); } else if (condition.Condition == ProfileConditionType.EqualsAny) { From 946c6b9981145d73a6cd64fc6fbcbd6d5b6961ae Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 11 Mar 2026 21:20:14 +0100 Subject: [PATCH 12/94] Return BadRequest when an invalid set of filters is given --- .../Library/LibraryManager.cs | 2 +- Jellyfin.Api/Controllers/ArtistsController.cs | 68 +----------------- .../Controllers/ChannelsController.cs | 70 +----------------- Jellyfin.Api/Controllers/ItemsController.cs | 34 +-------- .../Entities/InternalItemsQuery.cs | 71 +++++++++++++++++++ .../Entities/InternalItemsQueryTests.cs | 26 +++++++ 6 files changed, 104 insertions(+), 167 deletions(-) create mode 100644 tests/Jellyfin.Controller.Tests/Entities/InternalItemsQueryTests.cs diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f7f5c387e1..eee87c4d8b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2289,7 +2289,7 @@ namespace Emby.Server.Implementations.Library if (item is null) { - return new List(); + return []; } return GetCollectionFoldersInternal(item, allUserRootChildren); diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 642790f942..99b0fde06d 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -187,39 +187,7 @@ public class ArtistsController : BaseJellyfinApiController }).Where(i => i is not null).Select(i => i!.Id).ToArray(); } - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } + query.ApplyFilters(filters); var result = _libraryManager.GetArtists(query); @@ -390,39 +358,7 @@ public class ArtistsController : BaseJellyfinApiController }).Where(i => i is not null).Select(i => i!.Id).ToArray(); } - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } + query.ApplyFilters(filters); var result = _libraryManager.GetAlbumArtists(query); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 880b3a82d4..0d85b3a0db 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -136,45 +136,13 @@ public class ChannelsController : BaseJellyfinApiController { Limit = limit, StartIndex = startIndex, - ChannelIds = new[] { channelId }, + ChannelIds = [channelId], ParentId = folderId ?? Guid.Empty, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), DtoOptions = new DtoOptions { Fields = fields } }; - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - } - } + query.ApplyFilters(filters); return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false); } @@ -215,39 +183,7 @@ public class ChannelsController : BaseJellyfinApiController DtoOptions = new DtoOptions { Fields = fields } }; - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - } - } + query.ApplyFilters(filters); return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 9674ecd092..091a0c8c73 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -386,39 +386,7 @@ public class ItemsController : BaseJellyfinApiController query.CollapseBoxSetItems = false; } - foreach (var filter in filters) - { - switch (filter) - { - case ItemFilter.Dislikes: - query.IsLiked = false; - break; - case ItemFilter.IsFavorite: - query.IsFavorite = true; - break; - case ItemFilter.IsFavoriteOrLikes: - query.IsFavoriteOrLiked = true; - break; - case ItemFilter.IsFolder: - query.IsFolder = true; - break; - case ItemFilter.IsNotFolder: - query.IsFolder = false; - break; - case ItemFilter.IsPlayed: - query.IsPlayed = true; - break; - case ItemFilter.IsResumable: - query.IsResumable = true; - break; - case ItemFilter.IsUnplayed: - query.IsPlayed = false; - break; - case ItemFilter.Likes: - query.IsLiked = true; - break; - } - } + query.ApplyFilters(filters); // Filter by Series Status if (seriesStatus.Length != 0) diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 076a592922..ecbeefbb9d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -10,6 +10,7 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities { @@ -388,5 +389,75 @@ namespace MediaBrowser.Controller.Entities User = user; } + + public void ApplyFilters(ItemFilter[] filters) + { + static void ThrowConflictingFilters() + => throw new ArgumentException("Conflicting filters", nameof(filters)); + + foreach (var filter in filters) + { + switch (filter) + { + case ItemFilter.IsFolder: + if (filters.Contains(ItemFilter.IsNotFolder)) + { + ThrowConflictingFilters(); + } + + IsFolder = true; + break; + case ItemFilter.IsNotFolder: + if (filters.Contains(ItemFilter.IsFolder)) + { + ThrowConflictingFilters(); + } + + IsFolder = false; + break; + case ItemFilter.IsUnplayed: + if (filters.Contains(ItemFilter.IsPlayed)) + { + ThrowConflictingFilters(); + } + + IsPlayed = false; + break; + case ItemFilter.IsPlayed: + if (filters.Contains(ItemFilter.IsUnplayed)) + { + ThrowConflictingFilters(); + } + + IsPlayed = true; + break; + case ItemFilter.IsFavorite: + IsFavorite = true; + break; + case ItemFilter.IsResumable: + IsResumable = true; + break; + case ItemFilter.Likes: + if (filters.Contains(ItemFilter.Dislikes)) + { + ThrowConflictingFilters(); + } + + IsLiked = true; + break; + case ItemFilter.Dislikes: + if (filters.Contains(ItemFilter.Likes)) + { + ThrowConflictingFilters(); + } + + IsLiked = false; + break; + case ItemFilter.IsFavoriteOrLikes: + IsFavoriteOrLiked = true; + break; + } + } + } } } 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 ApplyFilters_Invalid() + { + var data = new TheoryData(); + 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(() => query.ApplyFilters(filters)); + } +} From b825829191803f511ee09bfa11237e213d4e69eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:24:35 +0000 Subject: [PATCH 13/94] Update dependency dotnet-ef to v10.0.5 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 302ac67b6f..9cd9c08e75 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.3", + "version": "10.0.5", "commands": [ "dotnet-ef" ] From 4952e65a0312cb5b8160d1621abb51ae1086ac3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:24:43 +0000 Subject: [PATCH 14/94] Update Microsoft to 10.0.5 --- Directory.Packages.props | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bd720ed348..294cb45b13 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,27 +26,27 @@ - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -77,7 +77,7 @@ - + From 3997e016fa092f8fad171ce5f24f619b552e5f78 Mon Sep 17 00:00:00 2001 From: lowbit Date: Fri, 13 Mar 2026 15:33:06 -0400 Subject: [PATCH 15/94] Backport pull request #16257 from jellyfin/release-10.11.z Fix subtitle extraction caching empty files Original-merge: 6864e108b8b36ad25655e683b2cf2abf8b8ca346 Merged-by: joshuaboniface Backported-by: Bond_009 --- .../Subtitles/SubtitleEncoder.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index bf7ec05a96..aeaf7f4423 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -328,7 +328,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } @@ -431,9 +431,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } } - else if (!File.Exists(outputPath)) + else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { failed = true; + + try + { + _logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath); + } } if (failed) @@ -507,7 +520,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); - if (File.Exists(outputPath)) + if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0) { releaser.Dispose(); continue; @@ -722,10 +735,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles { foreach (var outputPath in outputPaths) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); failed = true; + + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } + continue; } @@ -764,7 +791,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -867,9 +894,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); } } - else if (!File.Exists(outputPath)) + else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { failed = true; + + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } } if (failed) From cf03e3118a4b532d5a473119713b9f5f0c5e87d9 Mon Sep 17 00:00:00 2001 From: IceStormNG Date: Fri, 13 Mar 2026 15:33:07 -0400 Subject: [PATCH 16/94] Backport pull request #16293 from jellyfin/release-10.11.z Apply analyzeduration and probesize for subtitle streams to improve codec parameter detection Original-merge: fda49a5a49c2b6eadeb5f9b1b1bb683d536973f3 Merged-by: Bond-009 Backported-by: Bond_009 --- .../MediaEncoding/EncodingHelper.cs | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 11eee1a372..c7b11f47d1 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1267,6 +1267,20 @@ namespace MediaBrowser.Controller.MediaEncoding } } + // Use analyzeduration also for subtitle streams to improve resolution detection with streams inside MKS files + var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state); + if (!string.IsNullOrEmpty(analyzeDurationArgument)) + { + arg.Append(' ').Append(analyzeDurationArgument); + } + + // Apply probesize, too, if configured + var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg(); + if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument)) + { + arg.Append(' ').Append(ffmpegProbeSizeArgument); + } + // Also seek the external subtitles stream. var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer); if (!string.IsNullOrEmpty(seekSubParam)) @@ -7123,9 +7137,8 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer) + private string GetFfmpegAnalyzeDurationArg(EncodingJobInfo state) { - var inputModifier = string.Empty; var analyzeDurationArgument = string.Empty; // Apply -analyzeduration as per the environment variable, @@ -7141,6 +7154,26 @@ namespace MediaBrowser.Controller.MediaEncoding analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration; } + return analyzeDurationArgument; + } + + private string GetFfmpegProbesizeArg() + { + var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + return $"-probesize {ffmpegProbeSize}"; + } + + return string.Empty; + } + + public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer) + { + var inputModifier = string.Empty; + var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state); + if (!string.IsNullOrEmpty(analyzeDurationArgument)) { inputModifier += " " + analyzeDurationArgument; @@ -7149,11 +7182,11 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); // Apply -probesize if configured - var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg(); - if (!string.IsNullOrEmpty(ffmpegProbeSize)) + if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument)) { - inputModifier += $" -probesize {ffmpegProbeSize}"; + inputModifier += " " + ffmpegProbeSizeArgument; } var userAgentParam = GetUserAgentParam(state); From 31adb5dcd135efd1f790fd9acbdc6b50aee6afe4 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 13 Mar 2026 15:33:08 -0400 Subject: [PATCH 17/94] Backport pull request #16392 from jellyfin/release-10.11.z Fix filter detection in FFmpeg 8.1 Original-merge: 55c00d76bbbe2d2759f33fab673f26b26093a30e Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f4e8c39c11..68d6d215b2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -693,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder [GeneratedRegex("^\\s\\S{6}\\s(?[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] private static partial Regex CodecRegex(); - [GeneratedRegex("^\\s\\S{3}\\s(?[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] + [GeneratedRegex("^\\s\\S{2,3}\\s(?[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] private static partial Regex FilterRegex(); } } From d65960fe5d3f798ea3ac0527abb96b3327a39ba4 Mon Sep 17 00:00:00 2001 From: Benjamin Lea Date: Fri, 13 Mar 2026 17:30:07 -0400 Subject: [PATCH 18/94] Translated using Weblate (Hebrew (Israel)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he_IL/ --- .../Localization/Core/he_IL.json | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json index 1d688f01a3..e8812c8a1d 100644 --- a/Emby.Server.Implementations/Localization/Core/he_IL.json +++ b/Emby.Server.Implementations/Localization/Core/he_IL.json @@ -4,5 +4,24 @@ "Channels": "ערוצים", "Movies": "סרטים", "Music": "מוזיקה", - "Collections": "אוספים" + "Collections": "אוספים", + "Albums": "אלבומים", + "Application": "אפליקציה", + "Artists": "אמנים", + "ChapterNameValue": "פרק {0}", + "External": "חיצונית", + "Favorites": "מועדפים", + "Folders": "תיקיות", + "Genres": "ז'אנרים", + "HeaderAlbumArtists": "אמני אלבומים", + "HeaderContinueWatching": "להמשיך לצפות", + "HeaderFavoriteAlbums": "אלבומים אהובים", + "HeaderFavoriteArtists": "אמנים אהובים", + "HeaderFavoriteEpisodes": "פרקים אהובים", + "HeaderFavoriteShows": "תוכניות אהובות", + "HeaderFavoriteSongs": "שירים אהובים", + "HeaderLiveTV": "טלוויזיה בשידור חי", + "HeaderNextUp": "הבא", + "HearingImpaired": "ללקויי שמיעה", + "HomeVideos": "סרטונים ביתיים" } From 98bbc26c5e8260ae7e5f7f8e423c2480d35ca047 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 14 Mar 2026 19:58:43 +0100 Subject: [PATCH 19/94] Add callback for segment data pruning to IMediaSegmentProvider --- .../MediaSegments/MediaSegmentManager.cs | 12 ++++++++++++ .../MediaSegments/IMediaSegmentProvider.cs | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index d00c87463c..bcf1296331 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -187,6 +187,18 @@ public class MediaSegmentManager : IMediaSegmentManager { await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); } + + foreach (var provider in _segmentProviders) + { + try + { + await provider.CleanupExtractedData(itemId, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Provider {ProviderName} failed to clean up extracted data for item {ItemId}", provider.Name, itemId); + } + } } /// diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 5a6d15d781..ef0135900b 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -31,4 +32,16 @@ public interface IMediaSegmentProvider /// The base item to extract segments from. /// True if item is supported, otherwise false. ValueTask Supports(BaseItem item); + + /// + /// Called when extracted segment data for an item is being pruned. + /// Providers should delete any cached analysis data they hold for the given item. + /// + /// The item whose data is being pruned. + /// Abort token. + /// A task representing the asynchronous cleanup operation. + Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } From c0c474021519d4485a3f5724fcae1be8ffc45ba8 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Sat, 14 Mar 2026 14:37:46 -0400 Subject: [PATCH 20/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index e57a0c5b09..b8848e90d8 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -85,7 +85,7 @@ "UserOfflineFromDevice": "{0} 終止了 {1} 的連接", "UserOnlineFromDevice": "{0} 從 {1} 連線", "UserPasswordChangedWithName": "{0} 的密碼已被更改", - "UserPolicyUpdatedWithName": "使用條款已更新為 {0}", + "UserPolicyUpdatedWithName": "使用條款已更新爲 {0}", "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}", "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫", @@ -106,7 +106,7 @@ "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", "TaskCleanLogs": "清理紀錄檔資料夾", "TaskRefreshLibrary": "掃描媒體庫", - "TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。", + "TaskRefreshChapterImagesDescription": "爲帶有章節的影片建立縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", "TaskCleanCache": "清理緩存資料夾", @@ -125,7 +125,7 @@ "External": "外部", "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", - "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。", + "TaskRefreshTrickplayImagesDescription": "爲已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。", "TaskDownloadMissingLyrics": "下載欠缺歌詞", From f6a5b27efc89cfaea15c01876fc66cbb9007abe0 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Sun, 15 Mar 2026 02:22:50 -0400 Subject: [PATCH 21/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index b8848e90d8..37ac79a29f 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -85,7 +85,7 @@ "UserOfflineFromDevice": "{0} 終止了 {1} 的連接", "UserOnlineFromDevice": "{0} 從 {1} 連線", "UserPasswordChangedWithName": "{0} 的密碼已被更改", - "UserPolicyUpdatedWithName": "使用條款已更新爲 {0}", + "UserPolicyUpdatedWithName": "{0} 嘅用戶權限已經更新咗", "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}", "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫", @@ -106,7 +106,7 @@ "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", "TaskCleanLogs": "清理紀錄檔資料夾", "TaskRefreshLibrary": "掃描媒體庫", - "TaskRefreshChapterImagesDescription": "爲帶有章節的影片建立縮圖。", + "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", "TaskCleanCache": "清理緩存資料夾", @@ -125,7 +125,7 @@ "External": "外部", "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", - "TaskRefreshTrickplayImagesDescription": "爲已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。", + "TaskRefreshTrickplayImagesDescription": "幫已啟用功能嘅媒體庫影片整快轉預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。", "TaskDownloadMissingLyrics": "下載欠缺歌詞", From dbc42bb8e236f9ead33ddd47d500b7d00f52805d Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Sun, 15 Mar 2026 03:24:21 -0400 Subject: [PATCH 22/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 37ac79a29f..98dada84aa 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -117,10 +117,10 @@ "Undefined": "未定義", "Forced": "強制", "Default": "預設", - "TaskOptimizeDatabaseDescription": "壓縮數據庫及釋放可用空間。完成任何會修改數據庫的工作(例如掃描媒體庫)後,執行此工作或可提升伺服器速度。", + "TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體庫或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。", "TaskOptimizeDatabase": "最佳化數據庫", "TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。", - "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)以建立更準確的 HLS playlist。此工作可能需要使用較長時間來完成。", + "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。", "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", "HearingImpaired": "聽力障礙", @@ -137,5 +137,5 @@ "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", "CleanupUserDataTask": "用戶資料清理工作", - "CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。" + "CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。" } From dbd58dd666b7fd0c8c855806a70346cdc7736ff1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:10:01 +0100 Subject: [PATCH 23/94] Update CI dependencies (#16417) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-tests.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 9eadf7632d..0f1463c0f0 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -28,13 +28,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 + uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 7586e826b9..fc32cc884d 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@2a82782178b2816d9d6960a7345fdd164791b323 # v5.5.3 + uses: danielpalme/ReportGenerator-GitHub-Action@cf6fe1b38ed5becc89ffe056c1f240825993be5b # v5.5.4 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From 54856dc026ee2b3046d6fbe531a3eb3220ff134d Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Mon, 16 Mar 2026 13:55:08 -0400 Subject: [PATCH 24/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 98dada84aa..172e8555cf 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -5,7 +5,7 @@ "Artists": "藝人", "AuthenticationSucceededWithUserName": "成功授權 {0}", "Books": "書籍", - "CameraImageUploadedFrom": "{0} 成功上傳一張新照片", + "CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", @@ -33,8 +33,8 @@ "LabelRunningTimeValue": "運作時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 已被更新", - "MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新", + "MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」已經更新咗", "MessageServerConfigurationUpdated": "已更新伺服器設定", "MixedContent": "混合內容", "Movies": "電影", @@ -43,7 +43,7 @@ "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知的季度", - "NewVersionIsAvailable": "有新版本的 Jellyfin 可供下載。", + "NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。", "NotificationOptionApplicationUpdateAvailable": "有可用的更新", "NotificationOptionApplicationUpdateInstalled": "完成更新應用程式", "NotificationOptionAudioPlayback": "播放音訊", @@ -72,8 +72,8 @@ "ServerNameNeedsToBeRestarted": "{0} 需要重啟", "Shows": "節目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "正在載入 Jellyfin,請稍後再試。", - "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", + "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。", + "SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗", "Sync": "同步", "System": "系統", "TvShows": "電視節目", @@ -84,31 +84,31 @@ "UserLockedOutWithName": "用戶 {0} 已被封鎖", "UserOfflineFromDevice": "{0} 終止了 {1} 的連接", "UserOnlineFromDevice": "{0} 從 {1} 連線", - "UserPasswordChangedWithName": "{0} 的密碼已被更改", - "UserPolicyUpdatedWithName": "{0} 嘅用戶權限已經更新咗", + "UserPasswordChangedWithName": "用戶 {0} 嘅密碼已經更改咗", + "UserPolicyUpdatedWithName": "用戶 {0} 嘅權限已經更新咗", "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}", - "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫", + "UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}", + "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本 {0}", "TaskDownloadMissingSubtitles": "下載欠缺字幕", "TaskUpdatePlugins": "更新插件", "TasksApplicationCategory": "應用程式", - "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增的檔案及重新載入元數據。", + "TaskRefreshLibraryDescription": "掃描媒體庫嚟搵新檔案,同時重新載入元數據。", "TasksMaintenanceCategory": "維護", - "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在網上搜尋欠缺的字幕。", + "TaskDownloadMissingSubtitlesDescription": "根據元數據設定,喺網上幫你搵返啲欠缺嘅字幕。", "TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。", "TaskRefreshChannels": "重新載入頻道", - "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", + "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。", "TaskCleanTranscode": "清理轉碼檔資料夾", - "TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。", - "TaskRefreshPeopleDescription": "更新你的媒體中有關的演員和導演的元數據。", - "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", + "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅插件進行下載同安裝。", + "TaskRefreshPeopleDescription": "更新媒體庫入面演員同導演嘅元數據。", + "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。", "TaskCleanLogs": "清理紀錄檔資料夾", "TaskRefreshLibrary": "掃描媒體庫", "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整縮圖。", "TaskRefreshChapterImages": "提取章節圖像", - "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", + "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅緩存檔案。", "TaskCleanCache": "清理緩存資料夾", "TasksChannelsCategory": "網絡頻道", "TasksLibraryCategory": "媒體庫", @@ -119,22 +119,22 @@ "Default": "預設", "TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體庫或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。", "TaskOptimizeDatabase": "最佳化數據庫", - "TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。", + "TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。", "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。", "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", - "TaskRefreshTrickplayImagesDescription": "幫已啟用功能嘅媒體庫影片整快轉預覽圖。", + "TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體庫影片製作快轉預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", - "TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。", + "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。", "TaskDownloadMissingLyrics": "下載欠缺歌詞", "TaskDownloadMissingLyricsDescription": "下載歌詞", "TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單", "TaskAudioNormalization": "音訊同等化", "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。", - "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", - "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", + "TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。", + "TaskMoveTrickplayImagesDescription": "根據媒體庫設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。", "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", "CleanupUserDataTask": "用戶資料清理工作", "CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。" From a2fba38954acc50ebff73a2e746f4aa7bdeabc8b Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Tue, 17 Mar 2026 09:17:28 -0400 Subject: [PATCH 25/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 172e8555cf..6c3e2e0e7f 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -1,6 +1,6 @@ { "Albums": "專輯", - "AppDeviceValues": "程式:{0},設備:{1}", + "AppDeviceValues": "程式:{0},裝置:{1}", "Application": "應用程式", "Artists": "藝人", "AuthenticationSucceededWithUserName": "成功授權 {0}", @@ -11,7 +11,7 @@ "Collections": "系列", "DeviceOfflineWithName": "{0} 已中斷連接", "DeviceOnlineWithName": "{0} 已連接", - "FailedLoginAttemptWithUserName": "{0} 登入失敗", + "FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗", "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", @@ -27,15 +27,15 @@ "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", - "ItemAddedWithName": "{0} 已被加入至媒體庫", - "ItemRemovedWithName": "{0} 已從媒體庫移除", + "ItemAddedWithName": "{0} 經已加咗入媒體庫", + "ItemRemovedWithName": "{0} 經已由媒體庫移除咗", "LabelIpAddressValue": "IP 地址:{0}", "LabelRunningTimeValue": "運作時間:{0}", "Latest": "最新", - "MessageApplicationUpdated": "Jellyfin 已被更新", + "MessageApplicationUpdated": "Jellyfin 經已更新咗", "MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本", "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」已經更新咗", - "MessageServerConfigurationUpdated": "已更新伺服器設定", + "MessageServerConfigurationUpdated": "伺服器設定經已更新咗", "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", @@ -69,7 +69,7 @@ "ProviderValue": "提供者:{0}", "ScheduledTaskFailedWithName": "{0} 執行失敗", "ScheduledTaskStartedWithName": "開始執行 {0}", - "ServerNameNeedsToBeRestarted": "{0} 需要重啟", + "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動", "Shows": "節目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。", @@ -78,15 +78,15 @@ "System": "系統", "TvShows": "電視節目", "User": "用戶", - "UserCreatedWithName": "建立新用戶 {0}", - "UserDeletedWithName": "用戶 {0} 已被移除", - "UserDownloadingItemWithValues": "{0} 正在下載 {1}", - "UserLockedOutWithName": "用戶 {0} 已被封鎖", - "UserOfflineFromDevice": "{0} 終止了 {1} 的連接", - "UserOnlineFromDevice": "{0} 從 {1} 連線", + "UserCreatedWithName": "經已建立咗新用戶 {0}", + "UserDeletedWithName": "用戶 {0} 已經被刪除", + "UserDownloadingItemWithValues": "{0} 下載緊 {1}", + "UserLockedOutWithName": "用戶 {0} 經已被鎖定", + "UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線", + "UserOnlineFromDevice": "{0} 正喺 {1} 連線", "UserPasswordChangedWithName": "用戶 {0} 嘅密碼已經更改咗", "UserPolicyUpdatedWithName": "用戶 {0} 嘅權限已經更新咗", - "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}", + "UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}", "UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", @@ -97,7 +97,7 @@ "TaskRefreshLibraryDescription": "掃描媒體庫嚟搵新檔案,同時重新載入元數據。", "TasksMaintenanceCategory": "維護", "TaskDownloadMissingSubtitlesDescription": "根據元數據設定,喺網上幫你搵返啲欠缺嘅字幕。", - "TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。", + "TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。", "TaskRefreshChannels": "重新載入頻道", "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。", "TaskCleanTranscode": "清理轉碼檔資料夾", @@ -106,7 +106,7 @@ "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。", "TaskCleanLogs": "清理紀錄檔資料夾", "TaskRefreshLibrary": "掃描媒體庫", - "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整縮圖。", + "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅緩存檔案。", "TaskCleanCache": "清理緩存資料夾", @@ -129,13 +129,13 @@ "TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。", "TaskDownloadMissingLyrics": "下載欠缺歌詞", - "TaskDownloadMissingLyricsDescription": "下載歌詞", - "TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單", + "TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞", + "TaskCleanCollectionsAndPlaylists": "清理媒體系列(Collections)同埋播放清單", "TaskAudioNormalization": "音訊同等化", - "TaskAudioNormalizationDescription": "掃描檔案裏的音訊同等化資料。", + "TaskAudioNormalizationDescription": "掃描檔案入面嘅音訊標准化(Audio Normalization)數據。", "TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。", - "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", - "CleanupUserDataTask": "用戶資料清理工作", + "TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置", + "CleanupUserDataTask": "清理用戶資料嘅任務", "CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。" } From d88dd1dd63dbcca66cd63c576eae80ca6e40b4c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:28:30 +0100 Subject: [PATCH 26/94] Update dependency coverlet.collector to 8.0.1 (#16428) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 294cb45b13..09524549ef 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + From dc4d3639e00c7f66e9eefffc3cf2bf1ad9e37669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84rik?= Date: Tue, 17 Mar 2026 16:26:49 -0400 Subject: [PATCH 27/94] Translated using Weblate (Swedish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/ --- Emby.Server.Implementations/Localization/Core/sv.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 23acd3c532..2393e21b10 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -76,7 +76,7 @@ "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}", "Sync": "Synk", "System": "System", - "TvShows": "TV-serier", + "TvShows": "Tv-serier", "User": "Användare", "UserCreatedWithName": "Användaren {0} har skapats", "UserDeletedWithName": "Användaren {0} har tagits bort", From 63a7c71e7750b83741def18328019de188eb4bf6 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Thu, 19 Mar 2026 04:49:05 -0400 Subject: [PATCH 28/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 6c3e2e0e7f..852848f0fa 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -3,14 +3,14 @@ "AppDeviceValues": "程式:{0},裝置:{1}", "Application": "應用程式", "Artists": "藝人", - "AuthenticationSucceededWithUserName": "成功授權 {0}", + "AuthenticationSucceededWithUserName": "{0} 成功通過驗證", "Books": "書籍", "CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", - "DeviceOfflineWithName": "{0} 已中斷連接", - "DeviceOnlineWithName": "{0} 已連接", + "DeviceOfflineWithName": "{0} 斷開咗連接", + "DeviceOnlineWithName": "{0} 連接咗", "FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗", "Favorites": "我的最愛", "Folders": "資料夾", @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} 經已加咗入媒體庫", "ItemRemovedWithName": "{0} 經已由媒體庫移除咗", "LabelIpAddressValue": "IP 地址:{0}", - "LabelRunningTimeValue": "運作時間:{0}", + "LabelRunningTimeValue": "運行時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 經已更新咗", "MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本", @@ -44,28 +44,28 @@ "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知的季度", "NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。", - "NotificationOptionApplicationUpdateAvailable": "有可用的更新", - "NotificationOptionApplicationUpdateInstalled": "完成更新應用程式", - "NotificationOptionAudioPlayback": "播放音訊", - "NotificationOptionAudioPlaybackStopped": "停止播放音訊", - "NotificationOptionCameraImageUploaded": "相片上傳", + "NotificationOptionApplicationUpdateAvailable": "有得更新應用程式", + "NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗", + "NotificationOptionAudioPlayback": "開始播放音訊", + "NotificationOptionAudioPlaybackStopped": "停咗播放音訊", + "NotificationOptionCameraImageUploaded": "相機相片上傳咗", "NotificationOptionInstallationFailed": "安裝失敗", - "NotificationOptionNewLibraryContent": "新增媒體", + "NotificationOptionNewLibraryContent": "加咗新內容", "NotificationOptionPluginError": "插件錯誤", "NotificationOptionPluginInstalled": "安裝插件", "NotificationOptionPluginUninstalled": "解除安裝插件", - "NotificationOptionPluginUpdateInstalled": "完成更新插件", - "NotificationOptionServerRestartRequired": "伺服器需要重啟", - "NotificationOptionTaskFailed": "排程工作執行失敗", - "NotificationOptionUserLockedOut": "封鎖用戶", - "NotificationOptionVideoPlayback": "播放影片", - "NotificationOptionVideoPlaybackStopped": "停止播放影片", + "NotificationOptionPluginUpdateInstalled": "插件更新好咗", + "NotificationOptionServerRestartRequired": "伺服器需要重新啟動", + "NotificationOptionTaskFailed": "排程工作失敗", + "NotificationOptionUserLockedOut": "用戶被鎖定咗", + "NotificationOptionVideoPlayback": "開始播放影片", + "NotificationOptionVideoPlaybackStopped": "停咗播放影片", "Photos": "相片", "Playlists": "播放清單", "Plugin": "插件", - "PluginInstalledWithName": "已安裝 {0}", - "PluginUninstalledWithName": "已移除 {0}", - "PluginUpdatedWithName": "已更新 {0}", + "PluginInstalledWithName": "裝好咗 {0}", + "PluginUninstalledWithName": "剷走咗 {0}", + "PluginUpdatedWithName": "更新好咗 {0}", "ProviderValue": "提供者:{0}", "ScheduledTaskFailedWithName": "{0} 執行失敗", "ScheduledTaskStartedWithName": "開始執行 {0}", @@ -89,9 +89,9 @@ "UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}", "UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體庫", - "ValueSpecialEpisodeName": "特典 - {0}", + "ValueSpecialEpisodeName": "特別篇 - {0}", "VersionNumber": "版本 {0}", - "TaskDownloadMissingSubtitles": "下載欠缺字幕", + "TaskDownloadMissingSubtitles": "下載漏咗嘅字幕", "TaskUpdatePlugins": "更新插件", "TasksApplicationCategory": "應用程式", "TaskRefreshLibraryDescription": "掃描媒體庫嚟搵新檔案,同時重新載入元數據。", @@ -100,20 +100,20 @@ "TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。", "TaskRefreshChannels": "重新載入頻道", "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。", - "TaskCleanTranscode": "清理轉碼檔資料夾", + "TaskCleanTranscode": "清理轉碼資料夾", "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅插件進行下載同安裝。", "TaskRefreshPeopleDescription": "更新媒體庫入面演員同導演嘅元數據。", "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。", - "TaskCleanLogs": "清理紀錄檔資料夾", + "TaskCleanLogs": "清理日誌資料夾", "TaskRefreshLibrary": "掃描媒體庫", "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。", - "TaskRefreshChapterImages": "提取章節圖像", - "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅緩存檔案。", - "TaskCleanCache": "清理緩存資料夾", + "TaskRefreshChapterImages": "擷取章節圖片", + "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅快取檔案。", + "TaskCleanCache": "清理快取(Cache)資料夾", "TasksChannelsCategory": "網絡頻道", "TasksLibraryCategory": "媒體庫", "TaskRefreshPeople": "重新載入人物", - "TaskCleanActivityLog": "清理活動記錄", + "TaskCleanActivityLog": "清理活動紀錄", "Undefined": "未定義", "Forced": "強制", "Default": "預設", @@ -124,11 +124,11 @@ "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", "HearingImpaired": "聽力障礙", - "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", + "TaskRefreshTrickplayImages": "產生搜畫預覽圖", "TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體庫影片製作快轉預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。", - "TaskDownloadMissingLyrics": "下載欠缺歌詞", + "TaskDownloadMissingLyrics": "下載缺失嘅歌詞", "TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞", "TaskCleanCollectionsAndPlaylists": "清理媒體系列(Collections)同埋播放清單", "TaskAudioNormalization": "音訊同等化", From 995d56d5ff37c010f91780f70a66f75ef95c5f25 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 19 Mar 2026 14:31:29 -0500 Subject: [PATCH 29/94] Recognize ".m4b", ".m4a", ".aac", ".flac", ".mp3", and ".opus" as an audio-book formats (#15377) * Recognize ".m4b" as an audio-book format - Has the resolver recognize the ".m4b" format as book - Jellyfin reverts to seeing the file as a music file without this check * Recognize ".m4a", ".aac", ".flac", and ".mp3" as an audio-book formats - All the formats supported in the docs will now be marked as type "Book" * Add ".opus" as a supported Audiobook format --------- Co-authored-by: Louis Sandoval --- .../Library/Resolvers/Books/BookResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 1e885aad6e..3ee1c757f2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { public class BookResolver : ItemResolver { - private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" }; + private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf", ".m4b", ".m4a", ".aac", ".flac", ".mp3", ".opus" }; protected override Book Resolve(ItemResolveArgs args) { From f6211a03dd3561478b1a6c600b5d78aae7b3050e Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Fri, 20 Mar 2026 15:31:36 -0400 Subject: [PATCH 30/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 852848f0fa..a42a33b1da 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -125,7 +125,7 @@ "External": "外部", "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "產生搜畫預覽圖", - "TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體庫影片製作快轉預覽圖。", + "TaskRefreshTrickplayImagesDescription": "爲已啟用功能嘅媒體庫影片製作快轉預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。", "TaskDownloadMissingLyrics": "下載缺失嘅歌詞", From f52005768a7022d7f5fe1d4622e818982d3a06f3 Mon Sep 17 00:00:00 2001 From: lednurb Date: Sun, 22 Mar 2026 02:26:44 -0400 Subject: [PATCH 31/94] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 534c64e93c..dbbe2cbd08 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -16,14 +16,14 @@ "Folders": "Mappen", "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", - "HeaderContinueWatching": "Verderkijken", + "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", "HeaderFavoriteEpisodes": "Favoriete afleveringen", "HeaderFavoriteShows": "Favoriete series", "HeaderFavoriteSongs": "Favoriete nummers", "HeaderLiveTV": "Live-tv", - "HeaderNextUp": "Als volgende", + "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", "Inherit": "Erven", From b82a2ced75484c4805e927178bac0702d2713f71 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Mon, 23 Mar 2026 17:06:22 -0400 Subject: [PATCH 32/94] Backport pull request #16423 from jellyfin/release-10.11.z Fix readrate options in FFmpeg 8.1 Original-merge: 29b236185701091f6719862b05bd7bda58d88475 Merged-by: Bond-009 Backported-by: Bond_009 --- .../MediaEncoding/EncodingHelper.cs | 13 ++++++++++++- src/Jellyfin.LiveTv/IO/EncodedRecorder.cs | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c7b11f47d1..21d4c36f68 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -85,6 +85,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); + private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0); private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); @@ -7226,8 +7227,10 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion); } + int readrate = 0; if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp) { + readrate = 1; inputModifier += " -re"; } else if (encodingOptions.EnableSegmentDeletion @@ -7238,7 +7241,15 @@ namespace MediaBrowser.Controller.MediaEncoding { // Set an input read rate limit 10x for using SegmentDeletion with stream-copy // to prevent ffmpeg from exiting prematurely (due to fast drive) - inputModifier += " -readrate 10"; + readrate = 10; + inputModifier += $" -readrate {readrate}"; + } + + // Set a larger catchup value to revert to the old behavior, + // otherwise, remuxing might stall due to this new option + if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption) + { + inputModifier += $" -readrate_catchup {readrate * 100}"; } var flags = new List(); diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs index be7ff52977..d877a0d124 100644 --- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs +++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs @@ -156,6 +156,13 @@ namespace Jellyfin.LiveTv.IO if (mediaSource.ReadAtNativeFramerate) { inputModifier += " -re"; + + // Set a larger catchup value to revert to the old behavior, + // otherwise, remuxing might stall due to this new option + if (_mediaEncoder.EncoderVersion >= new Version(8, 0)) + { + inputModifier += " -readrate_catchup 100"; + } } if (mediaSource.RequiresLooping) From 24ec04d89fc5d4c1dd8d4ae6f8ccead9ab84c91e Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:06:23 -0400 Subject: [PATCH 33/94] Backport pull request #16449 from jellyfin/release-10.11.z Fix NFO saver using wrong provider ID for collectionnumber Original-merge: ce867f9834106e82d40a5ee59ff6f698b1dcabcb Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 0217bded13..0757155aac 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -547,7 +547,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); } - if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection)) + if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection)) { writer.WriteElementString("collectionnumber", tmdbCollection); writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString()); From 386c4cb7236650061452026cac365229aa3b0cca Mon Sep 17 00:00:00 2001 From: scheilch Date: Tue, 24 Mar 2026 18:02:00 +0100 Subject: [PATCH 34/94] Fix int32 overflow in QSV rate-control parameter computation (#16376) Fix int32 overflow in QSV rate-control parameter computation --- .../MediaEncoding/EncodingHelper.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 21d4c36f68..9ebaef171d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1567,14 +1567,15 @@ namespace MediaBrowser.Controller.MediaEncoding int bitrate = state.OutputVideoBitrate.Value; - // Bit rate under 1000k is not allowed in h264_qsv + // Bit rate under 1000k is not allowed in h264_qsv. if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { bitrate = Math.Max(bitrate, 1000); } - // Currently use the same buffer size for all encoders - int bufsize = bitrate * 2; + // Currently use the same buffer size for all non-QSV encoders. + // Use long arithmetic to prevent int32 overflow for very high bitrate values. + int bufsize = (int)Math.Min((long)bitrate * 2, int.MaxValue); if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase)) { @@ -1604,7 +1605,13 @@ namespace MediaBrowser.Controller.MediaEncoding // Set (maxrate == bitrate + 1) to trigger VBR for better bitrate allocation // Set (rc_init_occupancy == 2 * bitrate) and (bufsize == 4 * bitrate) to deal with drastic scene changes - return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {bitrate + 1} -rc_init_occupancy {bitrate * 2} -bufsize {bitrate * 4}"); + // Use long arithmetic and clamp to int.MaxValue to prevent int32 overflow + // (e.g. bitrate * 4 wraps to a negative value for bitrates above ~537 million) + int qsvMaxrate = (int)Math.Min((long)bitrate + 1, int.MaxValue); + int qsvInitOcc = (int)Math.Min((long)bitrate * 2, int.MaxValue); + int qsvBufsize = (int)Math.Min((long)bitrate * 4, int.MaxValue); + + return FormattableString.Invariant($"{mbbrcOpt} -b:v {bitrate} -maxrate {qsvMaxrate} -rc_init_occupancy {qsvInitOcc} -bufsize {qsvBufsize}"); } if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase) From 8fc6f07d5ae2cf57c83b6ceb6bbd4f769a688f2b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:59:16 +0000 Subject: [PATCH 35/94] Update swashbuckle-aspnetcore monorepo to 10.1.6 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 09524549ef..68f89a0580 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,8 +75,8 @@ - - + + From 6b443bb2ec64009f9903388aa87868d30c5c42d7 Mon Sep 17 00:00:00 2001 From: dkanada Date: Wed, 25 Mar 2026 15:39:31 +0900 Subject: [PATCH 36/94] split openapi workflows between pull request and merge --- .github/workflows/openapi/__generate.yml | 44 +++++++ .../{ci-openapi.yml => openapi/merge.yml} | 110 ++---------------- .github/workflows/openapi/pull-request.yml | 72 ++++++++++++ .github/workflows/openapi/workflow-run.yml | 59 ++++++++++ 4 files changed, 185 insertions(+), 100 deletions(-) create mode 100644 .github/workflows/openapi/__generate.yml rename .github/workflows/{ci-openapi.yml => openapi/merge.yml} (64%) create mode 100644 .github/workflows/openapi/pull-request.yml create mode 100644 .github/workflows/openapi/workflow-run.yml diff --git a/.github/workflows/openapi/__generate.yml b/.github/workflows/openapi/__generate.yml new file mode 100644 index 0000000000..255cc49e82 --- /dev/null +++ b/.github/workflows/openapi/__generate.yml @@ -0,0 +1,44 @@ +name: OpenAPI Generate + +on: + workflow_call: + inputs: + ref: + required: true + type: string + repository: + required: true + type: string + artifact: + required: true + type: string + +permissions: + contents: read + +jobs: + main: + name: Main + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref }} + repository: ${{ inputs.repository }} + + - name: Configure .NET + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: '10.0.x' + + - name: Create File + run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests + + - name: Upload Artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ inputs.artifact }} + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json + retention-days: 14 + if-no-files-found: error diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/openapi/merge.yml similarity index 64% rename from .github/workflows/ci-openapi.yml rename to .github/workflows/openapi/merge.yml index f4fd0829b0..a996b2da6b 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/openapi/merge.yml @@ -1,118 +1,28 @@ -name: OpenAPI +name: OpenAPI Publish on: push: branches: - master tags: - 'v*' - pull_request: permissions: {} jobs: - openapi-head: - name: OpenAPI - HEAD - runs-on: ubuntu-latest - permissions: read-all - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - - - name: Setup .NET - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: '10.0.x' - - name: Generate openapi.json - run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - - - name: Upload openapi.json - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: openapi-head - retention-days: 14 - if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json - - openapi-base: - name: OpenAPI - BASE - if: ${{ github.base_ref != '' }} - runs-on: ubuntu-latest - permissions: read-all - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.event.pull_request.head.sha }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - fetch-depth: 0 - - - name: Checkout common ancestor - env: - HEAD_REF: ${{ github.head_ref }} - run: | - git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} - git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* - ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) - git checkout --progress --force $ANCESTOR_REF - - - name: Setup .NET - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: '10.0.x' - - name: Generate openapi.json - run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - - - name: Upload openapi.json - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: openapi-base - retention-days: 14 - if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json - - openapi-diff: - permissions: - pull-requests: write - - name: OpenAPI - Difference - if: ${{ github.event_name == 'pull_request' }} - runs-on: ubuntu-latest - needs: - - openapi-head - - openapi-base - steps: - - name: Download openapi-head - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openapi-head - path: openapi-head - - - name: Download openapi-base - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: openapi-base - path: openapi-base - - - name: Detect OpenAPI changes - id: openapi-diff - uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0 - with: - old-spec: openapi-base/openapi.json - new-spec: openapi-head/openapi.json - markdown: openapi-changelog.md - add-pr-comment: true - github-token: ${{ secrets.GITHUB_TOKEN }} - + publish-openapi: + name: OpenAPI - Publish Artifact + uses: ./.github/workflows/openapi/__generate.yml + with: + ref: ${{ github.sha }} + repository: ${{ github.repository }} + artifact: openapi-head publish-unstable: name: OpenAPI - Publish Unstable Spec if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} runs-on: ubuntu-latest needs: - - openapi-head + - publish-openapi steps: - name: Set unstable dated version id: version @@ -173,7 +83,7 @@ jobs: if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} runs-on: ubuntu-latest needs: - - openapi-head + - publish-openapi steps: - name: Set version number id: version diff --git a/.github/workflows/openapi/pull-request.yml b/.github/workflows/openapi/pull-request.yml new file mode 100644 index 0000000000..3071027823 --- /dev/null +++ b/.github/workflows/openapi/pull-request.yml @@ -0,0 +1,72 @@ +name: OpenAPI Check +on: + pull_request: + +jobs: + ancestor: + name: Common Ancestor + runs-on: ubuntu-latest + outputs: + base_ref: ${{ steps.ancestor.outputs.base_ref }} + steps: + - name: Checkout Repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + - name: Search History + id: ancestor + run: | + git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }} + git fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/* + + ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} HEAD) + + echo "ref: ${ANCESTOR_REF}" + + echo "base_ref=${ANCESTOR_REF}" >> "$GITHUB_OUTPUT" + + head: + name: Head Artifact + uses: ./.github/workflows/openapi/__generate.yml + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + artifact: openapi-head + + base: + name: Base Artifact + uses: ./.github/workflows/openapi/__generate.yml + needs: + - ancestor + with: + ref: ${{ needs.ancestor.outputs.base_ref }} + repository: ${{ github.event.pull_request.base.repo.full_name }} + artifact: openapi-base + + diff: + name: Generate Report + runs-on: ubuntu-latest + needs: + - head + - base + steps: + - name: Download Head + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openapi-head + path: openapi-head + - name: Download Base + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openapi-base + path: openapi-base + - name: Detect Changes + uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0 + id: openapi-diff + with: + old-spec: openapi-base/openapi.json + new-spec: openapi-head/openapi.json + markdown: openapi-changelog.md + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/openapi/workflow-run.yml b/.github/workflows/openapi/workflow-run.yml new file mode 100644 index 0000000000..9dbd2c40a0 --- /dev/null +++ b/.github/workflows/openapi/workflow-run.yml @@ -0,0 +1,59 @@ +name: OpenAPI Report + +on: + workflow_run: + workflows: + - OpenAPI Check + types: + - completed + +jobs: + metadata: + name: Generate Metadata + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + outputs: + pr_number: ${{ steps.pr_number.outputs.pr_number }} + steps: + - name: Get Pull Request Number + id: pr_number + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + API_RESPONSE=$(gh pr list --repo "${GITHUB_REPOSITORY}" --search "${HEAD_SHA}" --state open --json number) + PR_NUMBER=$(echo "${API_RESPONSE}" | jq '.[0].number') + + echo "repository: ${GITHUB_REPOSITORY}" + echo "sha: ${HEAD_SHA}" + echo "response: ${API_RESPONSE}" + echo "pr: ${PR_NUMBER}" + + echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + comment: + name: Pull Request Comment + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + needs: + - metadata + permissions: + pull-requests: write + actions: read + contents: read + steps: + - name: Download OpenAPI Report + id: download_report + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: openapi-diff-report + path: openapi-diff-report + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Push Comment + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 + with: + github-token: ${{ secrets.JF_BOT_TOKEN }} + file-path: ${{ steps.download_report.outputs.download-path }}/openapi-changelog.md + pr-number: ${{ needs.metadata.outputs.pr_number }} + comment-tag: openapi-report From 5fa865f9e5b89878a2229c71c751aa5c16a36d71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:01:19 +0000 Subject: [PATCH 37/94] Update swashbuckle-aspnetcore monorepo to 10.1.7 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 68f89a0580..3385ee070a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,8 +75,8 @@ - - + + From 9c09e7113e9eaadcf691e0fae68256a940a8b989 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Wed, 25 Mar 2026 19:40:34 -0400 Subject: [PATCH 38/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index a42a33b1da..53c472804e 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -27,14 +27,14 @@ "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", - "ItemAddedWithName": "{0} 經已加咗入媒體庫", - "ItemRemovedWithName": "{0} 經已由媒體庫移除咗", + "ItemAddedWithName": "{0} 經已加咗入媒體櫃", + "ItemRemovedWithName": "{0} 經已由媒體櫃移除咗", "LabelIpAddressValue": "IP 地址:{0}", "LabelRunningTimeValue": "運行時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 經已更新咗", "MessageApplicationUpdatedTo": "Jellyfin 已經更新到 {0} 版本", - "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」已經更新咗", + "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定「{0}」經已更新咗", "MessageServerConfigurationUpdated": "伺服器設定經已更新咗", "MixedContent": "混合內容", "Movies": "電影", @@ -51,18 +51,18 @@ "NotificationOptionCameraImageUploaded": "相機相片上傳咗", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "加咗新內容", - "NotificationOptionPluginError": "插件錯誤", - "NotificationOptionPluginInstalled": "安裝插件", - "NotificationOptionPluginUninstalled": "解除安裝插件", - "NotificationOptionPluginUpdateInstalled": "插件更新好咗", + "NotificationOptionPluginError": "外掛程式錯誤", + "NotificationOptionPluginInstalled": "安裝外掛程式", + "NotificationOptionPluginUninstalled": "解除安裝外掛程式", + "NotificationOptionPluginUpdateInstalled": "外掛程式更新好咗", "NotificationOptionServerRestartRequired": "伺服器需要重新啟動", "NotificationOptionTaskFailed": "排程工作失敗", - "NotificationOptionUserLockedOut": "用戶被鎖定咗", + "NotificationOptionUserLockedOut": "用家被鎖定咗", "NotificationOptionVideoPlayback": "開始播放影片", "NotificationOptionVideoPlaybackStopped": "停咗播放影片", "Photos": "相片", "Playlists": "播放清單", - "Plugin": "插件", + "Plugin": "外掛程式", "PluginInstalledWithName": "裝好咗 {0}", "PluginUninstalledWithName": "剷走咗 {0}", "PluginUpdatedWithName": "更新好咗 {0}", @@ -77,47 +77,47 @@ "Sync": "同步", "System": "系統", "TvShows": "電視節目", - "User": "用戶", - "UserCreatedWithName": "經已建立咗新用戶 {0}", - "UserDeletedWithName": "用戶 {0} 已經被刪除", + "User": "使用者", + "UserCreatedWithName": "經已建立咗新使用者 {0}", + "UserDeletedWithName": "使用者 {0} 經已被刪除", "UserDownloadingItemWithValues": "{0} 下載緊 {1}", - "UserLockedOutWithName": "用戶 {0} 經已被鎖定", + "UserLockedOutWithName": "使用者 {0} 經已被鎖定", "UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線", "UserOnlineFromDevice": "{0} 正喺 {1} 連線", - "UserPasswordChangedWithName": "用戶 {0} 嘅密碼已經更改咗", - "UserPolicyUpdatedWithName": "用戶 {0} 嘅權限已經更新咗", + "UserPasswordChangedWithName": "使用者 {0} 嘅密碼經已更改咗", + "UserPolicyUpdatedWithName": "使用者 {0} 嘅權限經已更新咗", "UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}", "UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}", - "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體庫", + "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃", "ValueSpecialEpisodeName": "特別篇 - {0}", "VersionNumber": "版本 {0}", "TaskDownloadMissingSubtitles": "下載漏咗嘅字幕", - "TaskUpdatePlugins": "更新插件", + "TaskUpdatePlugins": "更新外掛程式", "TasksApplicationCategory": "應用程式", - "TaskRefreshLibraryDescription": "掃描媒體庫嚟搵新檔案,同時重新載入元數據。", + "TaskRefreshLibraryDescription": "掃描媒體櫃嚟搵新檔案,同時重新載入媒體詳細資料。", "TasksMaintenanceCategory": "維護", - "TaskDownloadMissingSubtitlesDescription": "根據元數據設定,喺網上幫你搵返啲欠缺嘅字幕。", + "TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。", "TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。", "TaskRefreshChannels": "重新載入頻道", "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。", "TaskCleanTranscode": "清理轉碼資料夾", - "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅插件進行下載同安裝。", - "TaskRefreshPeopleDescription": "更新媒體庫入面演員同導演嘅元數據。", + "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。", + "TaskRefreshPeopleDescription": "更新媒體櫃入面演員同導演嘅媒體詳細資料。", "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。", "TaskCleanLogs": "清理日誌資料夾", - "TaskRefreshLibrary": "掃描媒體庫", + "TaskRefreshLibrary": "掃描媒體櫃", "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。", "TaskRefreshChapterImages": "擷取章節圖片", "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅快取檔案。", "TaskCleanCache": "清理快取(Cache)資料夾", - "TasksChannelsCategory": "網絡頻道", - "TasksLibraryCategory": "媒體庫", + "TasksChannelsCategory": "網路頻道", + "TasksLibraryCategory": "媒體櫃", "TaskRefreshPeople": "重新載入人物", "TaskCleanActivityLog": "清理活動紀錄", "Undefined": "未定義", "Forced": "強制", - "Default": "預設", - "TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體庫或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。", + "Default": "初始", + "TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。", "TaskOptimizeDatabase": "最佳化數據庫", "TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。", "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。", @@ -125,17 +125,17 @@ "External": "外部", "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "產生搜畫預覽圖", - "TaskRefreshTrickplayImagesDescription": "爲已啟用功能嘅媒體庫影片製作快轉預覽圖。", + "TaskRefreshTrickplayImagesDescription": "爲已啟用功能嘅媒體櫃影片製作快轉預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", - "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅插件入面提取媒體片段。", + "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅外掛程式入面提取媒體片段。", "TaskDownloadMissingLyrics": "下載缺失嘅歌詞", "TaskDownloadMissingLyricsDescription": "幫啲歌下載歌詞", "TaskCleanCollectionsAndPlaylists": "清理媒體系列(Collections)同埋播放清單", "TaskAudioNormalization": "音訊同等化", "TaskAudioNormalizationDescription": "掃描檔案入面嘅音訊標准化(Audio Normalization)數據。", "TaskCleanCollectionsAndPlaylistsDescription": "自動清理資料庫同播放清單入面已經唔存在嘅項目。", - "TaskMoveTrickplayImagesDescription": "根據媒體庫設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。", + "TaskMoveTrickplayImagesDescription": "根據媒體櫃設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。", "TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置", - "CleanupUserDataTask": "清理用戶資料嘅任務", - "CleanupUserDataTaskDescription": "從用戶數據入面清除嗰啲已經被刪除咗超過 90 日嘅媒體相關資料。" + "CleanupUserDataTask": "清理使用者資料嘅任務", + "CleanupUserDataTaskDescription": "從使用者數據入面清除嗰啲經已被刪除咗超過 90 日嘅媒體相關資料。" } From 6e154de954e088bb47aaa833d51bc05afb6898be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:27:09 +0000 Subject: [PATCH 39/94] Pin chrisdickinson/setup-yq action to fa3192e --- .github/workflows/release-bump-version.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml index 4c6b6b8e75..963b4a6023 100644 --- a/.github/workflows/release-bump-version.yaml +++ b/.github/workflows/release-bump-version.yaml @@ -28,7 +28,7 @@ jobs: timeoutSeconds: 3600 - name: Setup YQ - uses: chrisdickinson/setup-yq@latest + uses: chrisdickinson/setup-yq@fa3192edd79d6eb0e4e12de8dde3a0c26f2b853b # latest with: yq-version: v4.9.8 From 40cadfca44f67f243bc18fcd388122ae2e72002e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:36:22 +0000 Subject: [PATCH 40/94] Update github/codeql-action action to v4.35.1 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 0f1463c0f0..5194c7df06 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -28,13 +28,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 From f33c039d1b24bfc2b45357c508690cb995fbce0c Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 27 Mar 2026 13:48:30 -0400 Subject: [PATCH 41/94] Fix BoxSet parentId being ignored in item queries --- Jellyfin.Api/Controllers/ItemsController.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 091a0c8c73..39760556a6 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -11,6 +11,7 @@ using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; @@ -270,15 +271,17 @@ public class ItemsController : BaseJellyfinApiController var dtoOptions = new DtoOptions { Fields = fields } .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - if (includeItemTypes.Length == 1 - && includeItemTypes[0] == BaseItemKind.BoxSet) - { - parentId = null; - } - var item = _libraryManager.GetParentItem(parentId, userId); QueryResult result; + if (includeItemTypes.Length == 1 + && includeItemTypes[0] == BaseItemKind.BoxSet + && item is not BoxSet) + { + parentId = null; + item = _libraryManager.GetUserRootFolder(); + } + if (item is not Folder folder) { folder = _libraryManager.GetUserRootFolder(); From 3fb1d94038ab755695b27e7f911b18d159eb1e8c Mon Sep 17 00:00:00 2001 From: Patrick Cunniff Date: Sat, 28 Mar 2026 16:25:01 -0400 Subject: [PATCH 42/94] reverse check for track changed Signed-off-by: Patrick Cunniff --- Emby.Server.Implementations/Session/SessionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 8e14f5bdf4..6b8888d244 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -960,7 +960,7 @@ namespace Emby.Server.Implementations.Session } var tracksChanged = UpdatePlaybackSettings(user, info, data); - if (!tracksChanged) + if (tracksChanged) { changed = true; } From e5bbb1ea0c0aa74ddb6f6d33c94583dfc1accf31 Mon Sep 17 00:00:00 2001 From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:12:06 +0400 Subject: [PATCH 43/94] Add spec-compliant dvh1 HLS variant for Dolby Vision Profile 5 (#16362) * Add spec-compliant dvh1 HLS variant for Dolby Vision Profile 5 DV Profile 5 has no backward-compatible base layer, so SUPPLEMENTAL-CODECS cannot be used. The master playlist currently labels P5 streams as hvc1 in the CODECS field, even though DynamicHlsController already passes -tag:v:0 dvh1 -strict -2 to FFmpeg for P5 copy-codec streams, writing a dvh1 FourCC and dvvC configuration box into the fMP4 init segment. This mismatch between the manifest (hvc1) and the bitstream (dvh1) causes spec-compliant clients like Apple TV and webOS 24+ to set up an HDR10 pipeline instead of a Dolby Vision one. Add a dvh1 variant before the existing hvc1 variant for P5 copy-codec streams. Both variants point to the same stream URL. Spec-compliant clients select dvh1 and activate the DV decoder path. Legacy clients that reject dvh1 in CODECS fall through to the hvc1 variant and detect DV from the init segment, preserving existing behavior. Fixes #16179 * Address review: support AV1 DoVi P10, add client capability check - GetDoviString: add isAv1 parameter, return dav1 FourCC for AV1 DoVi (P10 bl_compat_id=0) and dvh1 for HEVC DoVi (P5) - Remove redundant IsDovi() check; VideoRangeType.DOVI is sufficient and correctly limits to profiles without a compatible base layer - Replace IsDoviRemoved() with client capability check using GetRequestedRangeTypes(state.VideoStream.Codec) to only emit the dvh1/dav1 variant for clients that declared DOVI support - Update comments and doc summary to reflect P5 + P10/bl0 scope * Use codec string instead of boolean for DoVi FourCC mapping Replace bool isAv1 with string codec in GetDoviString for future-proofing when DoVi extends to H.266/VVC or AV2. * Move AppendDoviPlaylist next to AppendPlaylist * Fix SA1508: remove blank line before closing brace * Use AppendLine() instead of Append(Environment.NewLine) --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 78 +++++++++++++++++++ Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 21 +++++ 2 files changed, 99 insertions(+) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 44e1c6d5a2..b09b279699 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -209,6 +209,25 @@ public class DynamicHlsHelper AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); } + // For DoVi profiles without a compatible base layer (P5 HEVC, P10/bl0 AV1), + // add a spec-compliant dvh1/dav1 variant before the hvc1 hack variant. + // SUPPLEMENTAL-CODECS cannot be used for these profiles (no compatible BL to supplement). + // The DoVi variant is listed first so spec-compliant clients (Apple TV, webOS 24+) + // select it over the fallback when both have identical BANDWIDTH. + // Only emit for clients that explicitly declared DOVI support to avoid breaking + // non-compliant players that don't recognize dvh1/dav1 CODECS strings. + if (state.VideoStream is not null + && state.VideoRequest is not null + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.VideoRangeType == VideoRangeType.DOVI + && state.VideoStream.DvProfile.HasValue + && state.VideoStream.DvLevel.HasValue + && state.GetRequestedRangeTypes(state.VideoStream.Codec) + .Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase)) + { + AppendDoviPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); + } + var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); if (state.VideoStream is not null && state.VideoRequest is not null) @@ -355,6 +374,65 @@ public class DynamicHlsHelper return playlistBuilder; } + /// + /// Appends a Dolby Vision variant with dvh1/dav1 CODECS for profiles without a compatible + /// base layer (P5 HEVC, P10/bl0 AV1). This enables spec-compliant HLS clients to detect + /// DoVi from the manifest rather than relying on init segment inspection. + /// + /// StringBuilder for the master playlist. + /// StreamState of the current stream. + /// Playlist URL for this variant. + /// Bitrate for the BANDWIDTH field. + /// Subtitle group identifier, or null. + private void AppendDoviPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup) + { + var dvProfile = state.VideoStream.DvProfile; + var dvLevel = state.VideoStream.DvLevel; + if (dvProfile is null || dvLevel is null) + { + return; + } + + var playlistBuilder = new StringBuilder(); + playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)) + .Append(",AVERAGE-BANDWIDTH=") + .Append(bitrate.ToString(CultureInfo.InvariantCulture)); + + playlistBuilder.Append(",VIDEO-RANGE=PQ"); + + var dvCodec = HlsCodecStringHelpers.GetDoviString(dvProfile.Value, dvLevel.Value, state.ActualOutputVideoCodec); + + string audioCodecs = string.Empty; + if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec)) + { + audioCodecs = GetPlaylistAudioCodecs(state); + } + + playlistBuilder.Append(",CODECS=\"") + .Append(dvCodec); + if (!string.IsNullOrEmpty(audioCodecs)) + { + playlistBuilder.Append(',').Append(audioCodecs); + } + + playlistBuilder.Append('"'); + + AppendPlaylistResolutionField(playlistBuilder, state); + AppendPlaylistFramerateField(playlistBuilder, state); + + if (!string.IsNullOrWhiteSpace(subtitleGroup)) + { + playlistBuilder.Append(",SUBTITLES=\"") + .Append(subtitleGroup) + .Append('"'); + } + + playlistBuilder.AppendLine(); + playlistBuilder.AppendLine(url); + builder.Append(playlistBuilder); + } + /// /// Appends a VIDEO-RANGE field containing the range of the output video stream. /// diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index cf42d5f10b..1ac2abcfbf 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -346,4 +346,25 @@ public static class HlsCodecStringHelpers return result.ToString(); } + + /// + /// Gets a Dolby Vision codec string for profiles without a compatible base layer. + /// + /// Dolby Vision profile number. + /// Dolby Vision level number. + /// Video codec name (e.g. "hevc", "av1") to determine the DoVi FourCC. + /// Dolby Vision codec string. + public static string GetDoviString(int dvProfile, int dvLevel, string codec) + { + // HEVC DoVi uses dvh1, AV1 DoVi uses dav1 (out-of-band parameter sets, recommended by Apple HLS spec Rule 1.10) + var fourCc = string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1"; + StringBuilder result = new StringBuilder(fourCc, 12); + + result.Append('.') + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvProfile) + .Append('.') + .AppendFormat(CultureInfo.InvariantCulture, "{0:D2}", dvLevel); + + return result.ToString(); + } } From 6ea77f484d6c1b3faf160aee41d1ea099e64b85e Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 29 Mar 2026 12:38:01 +0200 Subject: [PATCH 44/94] Fix attachment extraction of files without video or audio stream (#16312) * Fix attachment extraction of files without video or audio stream * Apply review suggestions --- .../Attachments/AttachmentExtractor.cs | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 48a0654bb1..f7a1581a76 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -115,7 +115,6 @@ namespace MediaBrowser.MediaEncoding.Attachments await ExtractAllAttachmentsInternal( inputFile, mediaSource, - false, cancellationToken).ConfigureAwait(false); } } @@ -123,7 +122,6 @@ namespace MediaBrowser.MediaEncoding.Attachments private async Task ExtractAllAttachmentsInternal( string inputFile, MediaSourceInfo mediaSource, - bool isExternal, CancellationToken cancellationToken) { var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource); @@ -142,11 +140,19 @@ namespace MediaBrowser.MediaEncoding.Attachments return; } + // Files without video/audio streams (e.g. MKS subtitle files) don't need a dummy + // output since there are no streams to process. Omit "-t 0 -f null null" so ffmpeg + // doesn't fail trying to open an output with no streams. It will exit with code 1 + // ("at least one output file must be specified") which is expected and harmless + // since we only need the -dump_attachment side effect. + var hasVideoOrAudioStream = mediaSource.MediaStreams + .Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio); var processArgs = string.Format( CultureInfo.InvariantCulture, - "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null", + "-dump_attachment:t \"\" -y {0} -i {1} {2}", inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty, - inputPath); + inputPath, + hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty); int exitCode; @@ -185,12 +191,7 @@ namespace MediaBrowser.MediaEncoding.Attachments if (exitCode != 0) { - if (isExternal && exitCode == 1) - { - // ffmpeg returns exitCode 1 because there is no video or audio stream - // this can be ignored - } - else + if (hasVideoOrAudioStream || exitCode != 1) { failed = true; @@ -205,7 +206,8 @@ namespace MediaBrowser.MediaEncoding.Attachments } } } - else if (!Directory.Exists(outputFolder)) + + if (!failed && !Directory.Exists(outputFolder)) { failed = true; } @@ -246,6 +248,7 @@ namespace MediaBrowser.MediaEncoding.Attachments { await ExtractAttachmentInternal( _mediaEncoder.GetInputArgument(inputFile, mediaSource), + mediaSource, mediaAttachment.Index, attachmentPath, cancellationToken).ConfigureAwait(false); @@ -257,6 +260,7 @@ namespace MediaBrowser.MediaEncoding.Attachments private async Task ExtractAttachmentInternal( string inputPath, + MediaSourceInfo mediaSource, int attachmentStreamIndex, string outputPath, CancellationToken cancellationToken) @@ -267,12 +271,15 @@ namespace MediaBrowser.MediaEncoding.Attachments Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath))); + var hasVideoOrAudioStream = mediaSource.MediaStreams + .Any(s => s.Type == MediaStreamType.Video || s.Type == MediaStreamType.Audio); var processArgs = string.Format( CultureInfo.InvariantCulture, - "-dump_attachment:{1} \"{2}\" -i {0} -t 0 -f null null", + "-dump_attachment:{1} \"{2}\" -i {0} {3}", inputPath, attachmentStreamIndex, - EncodingUtils.NormalizePath(outputPath)); + EncodingUtils.NormalizePath(outputPath), + hasVideoOrAudioStream ? "-t 0 -f null null" : string.Empty); int exitCode; @@ -310,22 +317,26 @@ namespace MediaBrowser.MediaEncoding.Attachments if (exitCode != 0) { - failed = true; - - _logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode); - try + if (hasVideoOrAudioStream || exitCode != 1) { - if (File.Exists(outputPath)) + failed = true; + + _logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode); + try { - _fileSystem.DeleteFile(outputPath); + if (File.Exists(outputPath)) + { + _fileSystem.DeleteFile(outputPath); + } + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath); } } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath); - } } - else if (!File.Exists(outputPath)) + + if (!failed && !File.Exists(outputPath)) { failed = true; } From ad9ebe5baa166d26e15cb2472de7914c89fe7108 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 29 Mar 2026 12:38:32 +0200 Subject: [PATCH 45/94] More robust date handling in Library DB migration (#16474) * More robust date handling in Library DB migration * Apply review comment --- .../Migrations/Routines/MigrateLibraryDb.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index c6ac55b6eb..de55c00ec0 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -515,7 +515,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine PlayCount = dto.GetInt32(4), IsFavorite = dto.GetBoolean(5), PlaybackPositionTicks = dto.GetInt64(6), - LastPlayedDate = dto.IsDBNull(7) ? null : dto.GetDateTime(7), + LastPlayedDate = dto.IsDBNull(7) ? null : ReadDateTimeFromColumn(dto, 7), AudioStreamIndex = dto.IsDBNull(8) ? null : dto.GetInt32(8), SubtitleStreamIndex = dto.IsDBNull(9) ? null : dto.GetInt32(9), Likes = null, @@ -524,6 +524,28 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine }; } + private static DateTime? ReadDateTimeFromColumn(SqliteDataReader reader, int index) + { + // Try reading as a formatted date string first (handles ISO-8601 dates). + if (reader.TryReadDateTime(index, out var dateTimeResult)) + { + return dateTimeResult; + } + + // Some databases have Unix epoch timestamps stored as integers. + // SqliteDataReader.GetDateTime interprets integers as Julian dates, which crashes + // for Unix epoch values. Handle them explicitly. + var rawValue = reader.GetValue(index); + if (rawValue is long unixTimestamp + && unixTimestamp > 0 + && unixTimestamp <= DateTimeOffset.MaxValue.ToUnixTimeSeconds()) + { + return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime; + } + + return null; + } + private AncestorId GetAncestorId(SqliteDataReader reader) { return new AncestorId() From 921a364bb0b94b54f7a3215accdb0bc5f51ef9e7 Mon Sep 17 00:00:00 2001 From: furdiburd <93724729+furdiburd@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:38:46 +0200 Subject: [PATCH 46/94] Add ignore patterns for Hungarian sample files (#16238) * Add ignore patterns for Hungarian sample files Added ignore patterns for Hungarian sample files. * Removed leftover spaces --- .../Library/IgnorePatterns.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 59ccb9e2c7..197ec42c50 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -31,6 +31,20 @@ namespace Emby.Server.Implementations.Library "**/*.sample.?????", "**/sample/*", + // Avoid adding Hungarian sample files + // https://github.com/jellyfin/jellyfin/issues/16237 + "**/minta.?", + "**/minta.??", + "**/minta.???", // Matches minta.mkv + "**/minta.????", // Matches minta.webm + "**/minta.?????", + "**/*.minta.?", + "**/*.minta.??", + "**/*.minta.???", + "**/*.minta.????", + "**/*.minta.?????", + "**/minta/*", + // Directories "**/metadata/**", "**/metadata", From 5cfa466d8b0069e04c7d5c4e4f9b9a4bb7464034 Mon Sep 17 00:00:00 2001 From: scheilch Date: Sun, 29 Mar 2026 12:39:16 +0200 Subject: [PATCH 47/94] fix: cap GetVideoBitrateParamValue at 400 Mbps (#16467) * fix: cap GetVideoBitrateParamValue at 400 Mbps The previous cap of int.MaxValue / 2 (~1073 Mbps) is far beyond any realistic transcode target and allows encoder parameters derived from it (e.g. -bufsize = bitrate * 4 for QSV) to grow to multi-gigabit values, which is incorrect regardless of whether the encoder tolerates it. 400 Mbps is a safe upper bound for all current hardware encoders: - Intel QSV H.264 peaks at ~300 Mbps (High 5.1 CPB = 168.75 Mbit) - HEVC High Tier Level 5.x supports ~240 Mbps - AV1 hardware encoders have no meaningful real-world constraint at this level The existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps) provides a similar guard but only when LiveStreamId is set, covering M3U and HDHR sources. Plugin-provided streams and any source that bypasses the LiveTV pipeline are not subject to it and can pass unreasonably high values downstream. This cap closes that gap for all encoder paths. Suggested by @nyanmisaka in review of #16376. * Update MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs --------- Co-authored-by: Bond-009 --- .../MediaEncoding/EncodingHelper.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9ebaef171d..415ed3ea46 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2614,8 +2614,16 @@ namespace MediaBrowser.Controller.MediaEncoding } } - // Cap the max target bitrate to intMax/2 to satisfy the bufsize=bitrate*2. - return Math.Min(bitrate ?? 0, int.MaxValue / 2); + // Cap the max target bitrate to 400 Mbps. + // No consumer or professional hardware transcode target exceeds this value + // (Intel QSV tops out at ~300 Mbps for H.264; HEVC High Tier Level 5.x is ~240 Mbps). + // Without this cap, plugin-provided MPEG-TS streams with no usable bitrate metadata + // can produce unreasonably large -bufsize/-maxrate values for the encoder. + // Note: the existing FallbackMaxStreamingBitrate mechanism (default 30 Mbps) only + // applies when a LiveStreamId is set (M3U/HDHR sources). Plugin streams and other + // sources that bypass the LiveTV pipeline are not covered by it. + const int MaxSaneBitrate = 400_000_000; // 400 Mbps + return Math.Min(bitrate ?? 0, MaxSaneBitrate); } private int GetMinBitrate(int sourceBitrate, int requestedBitrate) From ea206f43a25700904bc9c909d6616dfe55ab8671 Mon Sep 17 00:00:00 2001 From: upscaylman <157367283+upscaylman@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:42:36 +0200 Subject: [PATCH 48/94] recognize underscore and dot separators for multi-version grouping (#16465) * Add underscore and dot as multi-version file separators Extend IsEligibleForMultiVersion to recognize _ and . as valid separators between the base movie name and the version suffix. Common naming patterns like 'Movie_4K.mkv' or 'Movie.UHD.mkv' are now correctly grouped as alternate versions during library scan. * Address review: remove comment, add 3D recognition assertions --------- Co-authored-by: aimarshall615-creator --- Emby.Naming/Video/VideoListResolver.cs | 2 + .../Video/MultiVersionTests.cs | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 4247fea0e5..a4bfb8d4a1 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -217,6 +217,8 @@ namespace Emby.Naming.Video // The CleanStringParser should have removed common keywords etc. return testFilename.IsEmpty || testFilename[0] == '-' + || testFilename[0] == '_' + || testFilename[0] == '.' || CheckMultiVersionRegex().IsMatch(testFilename); } } 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().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().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().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Single(result[0].AlternateVersions); + } } } From a6da575785e678e64ed03978d1f4f60a80423121 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 29 Mar 2026 14:16:26 +0200 Subject: [PATCH 49/94] 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(-) 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] From a3960b30c04d5313b0bfb3d57a7da35af5e8af3b Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sun, 29 Mar 2026 14:28:41 -0400 Subject: [PATCH 50/94] Backport pull request #16369 from jellyfin/release-10.11.z Fix nullref ex in font handling Original-merge: 41c2d51d8cb9b4f9bdf81be6e73f7ae2d447a8d7 Merged-by: Bond-009 Backported-by: Bond_009 --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 57 +++++++++++++++--------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index c6eab92ead..ade993d927 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -25,7 +25,7 @@ public class SkiaEncoder : IImageEncoder private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private static readonly SKImageFilter _imageFilter; - private static readonly SKTypeface[] _typefaces; + private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); /// /// The default sampling options, equivalent to old high quality filter settings when upscaling. @@ -37,9 +37,7 @@ public class SkiaEncoder : IImageEncoder /// public static readonly SKSamplingOptions DefaultSamplingOptions; -#pragma warning disable CA1810 static SkiaEncoder() -#pragma warning restore CA1810 { var kernel = new[] { @@ -59,21 +57,6 @@ public class SkiaEncoder : IImageEncoder SKShaderTileMode.Clamp, true); - // Initialize the list of typefaces - // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point - // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) - _typefaces = - [ - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic - SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font - ]; - // use cubic for upscaling UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); // use bilinear for everything else @@ -132,7 +115,7 @@ public class SkiaEncoder : IImageEncoder /// /// Gets the default typeface to use. /// - public static SKTypeface DefaultTypeFace => _typefaces.Last(); + public static SKTypeface? DefaultTypeFace => _typefaces.Last(); /// /// Check if the native lib is available. @@ -152,6 +135,40 @@ public class SkiaEncoder : IImageEncoder } } + /// + /// Initialize the list of typefaces + /// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point + /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) + /// + /// The list of typefaces. + private static SKTypeface?[] InitializeTypefaces() + { + int[] chars = [ + '鸡', // CJK Simplified Chinese + '雞', // CJK Traditional Chinese + 'ノ', // CJK Japanese + '각', // CJK Korean + 128169, // Emojis, 128169 is the Pile of Poo (💩) emoji + 'ז', // Hebrew + 'ي' // Arabic + ]; + var fonts = new List(chars.Length + 1); + foreach (var ch in chars) + { + var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch); + if (font is not null) + { + fonts.Add(font); + } + } + + // Default font + fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) + ?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a')); + + return fonts.ToArray(); + } + /// /// Convert a to a . /// @@ -809,7 +826,7 @@ public class SkiaEncoder : IImageEncoder { foreach (var typeface in _typefaces) { - if (typeface.ContainsGlyphs(c)) + if (typeface is not null && typeface.ContainsGlyphs(c)) { return typeface; } From acaeba11f3d2f61d1b23b36cf3c2d96f6a2e91bc Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 30 Mar 2026 00:12:35 +0200 Subject: [PATCH 51/94] Apply review comments --- .../MediaSegments/MediaSegmentManager.cs | 12 ++++++------ .../MediaSegments/IMediaSegmentProvider.cs | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index bcf1296331..c514735688 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -182,12 +182,6 @@ public class MediaSegmentManager : IMediaSegmentManager /// public async Task DeleteSegmentsAsync(Guid itemId, CancellationToken cancellationToken) { - var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (db.ConfigureAwait(false)) - { - await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } - foreach (var provider in _segmentProviders) { try @@ -199,6 +193,12 @@ public class MediaSegmentManager : IMediaSegmentManager _logger.LogError(ex, "Provider {ProviderName} failed to clean up extracted data for item {ItemId}", provider.Name, itemId); } } + + var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (db.ConfigureAwait(false)) + { + await db.MediaSegments.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } } /// diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index ef0135900b..54da218530 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -40,8 +40,5 @@ public interface IMediaSegmentProvider /// The item whose data is being pruned. /// Abort token. /// A task representing the asynchronous cleanup operation. - Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken); } From a0834973ede3ef388a3466070d686294d60d2692 Mon Sep 17 00:00:00 2001 From: krvi Date: Sun, 29 Mar 2026 14:17:38 -0400 Subject: [PATCH 52/94] Translated using Weblate (Faroese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fo/ --- Emby.Server.Implementations/Localization/Core/fo.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json index 40aa5f71a4..044abc7fa3 100644 --- a/Emby.Server.Implementations/Localization/Core/fo.json +++ b/Emby.Server.Implementations/Localization/Core/fo.json @@ -14,5 +14,9 @@ "DeviceOnlineWithName": "{0} er sambundið", "Favorites": "Yndis", "Folders": "Mappur", - "Forced": "Kravt" + "Forced": "Kravt", + "FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}", + "HeaderFavoriteEpisodes": "Yndispartar", + "HeaderFavoriteSongs": "Yndissangir", + "LabelIpAddressValue": "IP atsetur: {0}" } From c5ee639016f1ed4080e63f8dd6e98d10b56138e1 Mon Sep 17 00:00:00 2001 From: dkanada Date: Tue, 31 Mar 2026 01:41:47 +0900 Subject: [PATCH 53/94] remove nested directory for openapi workflows --- .../{openapi/__generate.yml => openapi-generate.yml} | 0 .github/workflows/{openapi/merge.yml => openapi-merge.yml} | 2 +- .../{openapi/pull-request.yml => openapi-pull-request.yml} | 4 ++-- .../{openapi/workflow-run.yml => openapi-workflow-run.yml} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{openapi/__generate.yml => openapi-generate.yml} (100%) rename .github/workflows/{openapi/merge.yml => openapi-merge.yml} (99%) rename .github/workflows/{openapi/pull-request.yml => openapi-pull-request.yml} (95%) rename .github/workflows/{openapi/workflow-run.yml => openapi-workflow-run.yml} (100%) diff --git a/.github/workflows/openapi/__generate.yml b/.github/workflows/openapi-generate.yml similarity index 100% rename from .github/workflows/openapi/__generate.yml rename to .github/workflows/openapi-generate.yml diff --git a/.github/workflows/openapi/merge.yml b/.github/workflows/openapi-merge.yml similarity index 99% rename from .github/workflows/openapi/merge.yml rename to .github/workflows/openapi-merge.yml index a996b2da6b..cd990cf5f8 100644 --- a/.github/workflows/openapi/merge.yml +++ b/.github/workflows/openapi-merge.yml @@ -11,7 +11,7 @@ permissions: {} jobs: publish-openapi: name: OpenAPI - Publish Artifact - uses: ./.github/workflows/openapi/__generate.yml + uses: ./.github/workflows/openapi-generate.yml with: ref: ${{ github.sha }} repository: ${{ github.repository }} diff --git a/.github/workflows/openapi/pull-request.yml b/.github/workflows/openapi-pull-request.yml similarity index 95% rename from .github/workflows/openapi/pull-request.yml rename to .github/workflows/openapi-pull-request.yml index 3071027823..b583fb54d1 100644 --- a/.github/workflows/openapi/pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -29,7 +29,7 @@ jobs: head: name: Head Artifact - uses: ./.github/workflows/openapi/__generate.yml + uses: ./.github/workflows/openapi-generate.yml with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -37,7 +37,7 @@ jobs: base: name: Base Artifact - uses: ./.github/workflows/openapi/__generate.yml + uses: ./.github/workflows/openapi-generate.yml needs: - ancestor with: diff --git a/.github/workflows/openapi/workflow-run.yml b/.github/workflows/openapi-workflow-run.yml similarity index 100% rename from .github/workflows/openapi/workflow-run.yml rename to .github/workflows/openapi-workflow-run.yml From 7825fa4e43f1970aa46d5ee0e986dee019bf4dd2 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:08:04 -0400 Subject: [PATCH 54/94] Backport pull request #16425 from jellyfin/release-10.11.z Fix restore backup metadata location Original-merge: 0f1732e5f5444cd6876dec816b5ff5822a93862b Merged-by: joshuaboniface Backported-by: Bond_009 --- .../FullSystemBackup/BackupService.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 30094a88c0..a6dc5458ee 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -118,15 +118,21 @@ public class BackupService : IBackupService throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version."); } - void CopyDirectory(string source, string target) + void CopyDirectory(string source, string target, string[]? exclude = null) { var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar); var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar; + var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray(); foreach (var item in zipArchive.Entries) { var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName)); var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName))); + if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal))) + { + continue; + } + if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal) || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal) || Path.EndsInDirectorySeparator(item.FullName)) @@ -142,8 +148,10 @@ public class BackupService : IBackupService } CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath); - CopyDirectory("Data", _applicationPaths.DataPath); + CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]); CopyDirectory("Root", _applicationPaths.RootFolderPath); + CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath); + CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath); if (manifest.Options.Database) { @@ -404,6 +412,15 @@ public class BackupService : IBackupService if (backupOptions.Metadata) { CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); + + // If a custom metadata path is configured, the default location may still contain data. + if (!string.Equals( + Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath), + Path.GetFullPath(_applicationPaths.InternalMetadataPath), + StringComparison.OrdinalIgnoreCase)) + { + CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default")); + } } var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false); From 42e8a780ca937b0f49b5e61b60adfda4abd465ec Mon Sep 17 00:00:00 2001 From: Molier Date: Mon, 30 Mar 2026 14:08:05 -0400 Subject: [PATCH 55/94] Backport pull request #16440 from jellyfin/release-10.11.z Remove -copyts and add -flush_packets 1 to subtitle extraction Original-merge: ec33c74ec44693a9ddb1e2f13bea90ef3c22267e Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index aeaf7f4423..9aeac7221e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -577,7 +577,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPaths = new List(); var args = string.Format( CultureInfo.InvariantCulture, - "-i {0} -copyts", + "-i {0}", inputPath); foreach (var subtitleStream in subtitleStreams) @@ -602,7 +602,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", streamIndex, outputCodec, outputPath); @@ -621,7 +621,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPaths = new List(); var args = string.Format( CultureInfo.InvariantCulture, - "-i {0} -copyts", + "-i {0}", inputPath); foreach (var subtitleStream in subtitleStreams) @@ -647,7 +647,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", streamIndex, outputCodec, outputPath); From 2134ea3f7fd12ef9bd7b5881cedb84484c6f56d2 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:08:06 -0400 Subject: [PATCH 56/94] Backport pull request #16514 from jellyfin/release-10.11.z Fix lint issue Original-merge: e1691e649e8431d080e1d1bc0aacbc2e6198f371 Merged-by: joshuaboniface Backported-by: Bond_009 --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index ade993d927..babab57d52 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -138,7 +138,7 @@ public class SkiaEncoder : IImageEncoder /// /// Initialize the list of typefaces /// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point - /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) + /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F). /// /// The list of typefaces. private static SKTypeface?[] InitializeTypefaces() From 5b3537b3d7260fb0ab0694d68ddc03e5495f262d Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Mon, 30 Mar 2026 14:08:07 -0400 Subject: [PATCH 57/94] Backport pull request #16519 from jellyfin/release-10.11.z Fix Null was not checked before using the H264 profile Original-merge: 89e914c7f18a6fcacf093d5f8df63b0d0506cbd5 Merged-by: Bond-009 Backported-by: Bond_009 --- .../MediaEncoding/EncodingHelper.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 415ed3ea46..f2468782ff 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6389,17 +6389,15 @@ namespace MediaBrowser.Controller.MediaEncoding } // Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format - if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) + && ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false) + || (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false))) { - if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) - || videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase)) + // VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P + if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox + && RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))) { - // VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P - if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox - && RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))) - { - return null; - } + return null; } } From d5d4309417ff6d39015db57c729cf55111df8ac0 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 30 Mar 2026 14:08:09 -0400 Subject: [PATCH 58/94] Backport pull request #16522 from jellyfin/release-10.11.z Fix CA1810 build error Original-merge: 7e88b18192762dcbf82b2182bacd486b4d828e04 Merged-by: nielsvanvelzen Backported-by: Bond_009 --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 43 ++++++++---------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index babab57d52..3f7ae4d2cd 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -24,44 +24,29 @@ public class SkiaEncoder : IImageEncoder private static readonly HashSet _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private static readonly SKImageFilter _imageFilter; private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); + private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution( + new SKSizeI(3, 3), + [ + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0 + ], + 1f, + 0f, + new SKPointI(1, 1), + SKShaderTileMode.Clamp, + true); /// /// The default sampling options, equivalent to old high quality filter settings when upscaling. /// - public static readonly SKSamplingOptions UpscaleSamplingOptions; + public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); /// /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling. /// - public static readonly SKSamplingOptions DefaultSamplingOptions; - - static SkiaEncoder() - { - var kernel = new[] - { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, - }; - - var kernelSize = new SKSizeI(3, 3); - var kernelOffset = new SKPointI(1, 1); - _imageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, - kernel, - 1f, - 0f, - kernelOffset, - SKShaderTileMode.Clamp, - true); - - // use cubic for upscaling - UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); - // use bilinear for everything else - DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear); - } + public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear); /// /// Initializes a new instance of the class. From b6e4b3a4f5e9079b673885339b2219f94c27f3c7 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Mon, 30 Mar 2026 11:04:55 -0400 Subject: [PATCH 59/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 53c472804e..1f8deb2c9e 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -12,16 +12,16 @@ "DeviceOfflineWithName": "{0} 斷開咗連接", "DeviceOnlineWithName": "{0} 連接咗", "FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗", - "Favorites": "我的最愛", + "Favorites": "心水", "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯歌手", "HeaderContinueWatching": "繼續觀看", - "HeaderFavoriteAlbums": "最愛的專輯", - "HeaderFavoriteArtists": "最愛的藝人", - "HeaderFavoriteEpisodes": "最愛的劇集", - "HeaderFavoriteShows": "最愛的節目", - "HeaderFavoriteSongs": "最愛的歌曲", + "HeaderFavoriteAlbums": "心水嘅專輯", + "HeaderFavoriteArtists": "心水嘅藝人", + "HeaderFavoriteEpisodes": "心水嘅劇集", + "HeaderFavoriteShows": "心水嘅節目", + "HeaderFavoriteSongs": "心水嘅歌曲", "HeaderLiveTV": "電視直播", "HeaderNextUp": "繼續觀看", "HeaderRecordingGroups": "錄製組", @@ -125,7 +125,7 @@ "External": "外部", "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "產生搜畫預覽圖", - "TaskRefreshTrickplayImagesDescription": "爲已啟用功能嘅媒體櫃影片製作快轉預覽圖。", + "TaskRefreshTrickplayImagesDescription": "為已啟用功能嘅媒體櫃影片製作快轉預覽圖。", "TaskExtractMediaSegments": "掃描媒體分段資訊", "TaskExtractMediaSegmentsDescription": "從支援 MediaSegment 功能嘅外掛程式入面提取媒體片段。", "TaskDownloadMissingLyrics": "下載缺失嘅歌詞", @@ -137,5 +137,5 @@ "TaskMoveTrickplayImagesDescription": "根據媒體櫃設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。", "TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置", "CleanupUserDataTask": "清理使用者資料嘅任務", - "CleanupUserDataTaskDescription": "從使用者數據入面清除嗰啲經已被刪除咗超過 90 日嘅媒體相關資料。" + "CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體使用者數據(包括觀看狀態、心水狀態等)。" } From cef81ae2ed4237510692d9a4b6286fc78f4c0193 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Tue, 31 Mar 2026 11:16:51 -0400 Subject: [PATCH 60/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 1f8deb2c9e..2e3fde2b04 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -117,8 +117,8 @@ "Undefined": "未定義", "Forced": "強制", "Default": "初始", - "TaskOptimizeDatabaseDescription": "壓縮數據庫並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據庫嘅操作之後行呢個任務,或者可以提升效能。", - "TaskOptimizeDatabase": "最佳化數據庫", + "TaskOptimizeDatabaseDescription": "壓縮數據櫃並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據櫃嘅操作之後行呢個任務,或者可以提升效能。", + "TaskOptimizeDatabase": "最佳化數據櫃", "TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。", "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。", "TaskKeyframeExtractor": "關鍵影格提取器", @@ -137,5 +137,5 @@ "TaskMoveTrickplayImagesDescription": "根據媒體櫃設定,將現有嘅 Trickplay(快轉預覽)檔案搬去對應位置。", "TaskMoveTrickplayImages": "搬移快轉預覽圖嘅位置", "CleanupUserDataTask": "清理使用者資料嘅任務", - "CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體使用者數據(包括觀看狀態、心水狀態等)。" + "CleanupUserDataTaskDescription": "清理已消失至少 90 日嘅媒體用家數據(包括觀看狀態、心水狀態等)。" } From 736a01f447c40ba116345709eced4f775da7f2bc Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 31 Mar 2026 22:05:37 -0400 Subject: [PATCH 61/94] Update issue template version to 10.11.7 --- .github/ISSUE_TEMPLATE/issue report.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 9bcff76bd8..909f22ed1d 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,13 +87,8 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: + - 10.11.7 - 10.11.6 - - 10.11.5 - - 10.11.4 - - 10.11.3 - - 10.11.2 - - 10.11.1 - - 10.11.0 - Master - Unstable - Older* From 7c57b62ece5991ca9e7dfdbf3f110d26c3a0e22a Mon Sep 17 00:00:00 2001 From: g10rga321 Date: Tue, 31 Mar 2026 15:45:11 -0400 Subject: [PATCH 62/94] Translated using Weblate (Georgian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ka/ --- .../Localization/Core/ka.json | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index 2d02522fea..79863a085b 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -9,46 +9,46 @@ "Artists": "არტისტი", "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", "Books": "წიგნები", - "Forced": "ძალით", + "Forced": "იძულებითი", "Inherit": "მემკვიდრეობით", "Latest": "უახლესი", "Movies": "ფილმები", "Music": "მუსიკა", "Photos": "ფოტოები", "Playlists": "დასაკრავი სიები", - "Plugin": "დამატება", + "Plugin": "მოდული", "Shows": "სერიალები", "Songs": "სიმღერები", "Sync": "სინქრონიზაცია", "System": "სისტემა", - "Undefined": "აღუწერელი", + "Undefined": "განუსაზღვრელი", "User": "მომხმარებელი", "TasksMaintenanceCategory": "რემონტი", "TasksLibraryCategory": "ბიბლიოთეკა", "ChapterNameValue": "თავი {0}", "HeaderContinueWatching": "ყურების გაგრძელება", "HeaderFavoriteArtists": "რჩეული შემსრულებლები", - "DeviceOfflineWithName": "{0} გაითიშა", + "DeviceOfflineWithName": "{0} გამოეთიშა", "External": "გარე", "HeaderFavoriteEpisodes": "რჩეული ეპიზოდები", "HeaderFavoriteSongs": "რჩეული სიმღერები", "HeaderRecordingGroups": "ჩამწერი ჯგუფები", "HearingImpaired": "სმენადაქვეითებული", - "LabelRunningTimeValue": "გაშვებულობის დრო: {0}", + "LabelRunningTimeValue": "ხანგრძლივობა: {0}", "MessageApplicationUpdatedTo": "Jellyfin-ის სერვერი განახლდა {0}-ზე", "MessageNamedServerConfigurationUpdatedWithValue": "სერვერის კონფიგურაციის სექცია {0} განახლდა", "MixedContent": "შერეული შემცველობა", - "MusicVideos": "მუსიკის ვიდეოები", + "MusicVideos": "მუსიკალური ვიდეოები", "NotificationOptionInstallationFailed": "დაყენების შეცდომა", "NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია", "NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია", "NotificationOptionCameraImageUploaded": "კამერის გამოსახულება ატვირთულია", "NotificationOptionVideoPlaybackStopped": "ვიდეოს დაკვრა გაჩერებულია", "PluginUninstalledWithName": "{0} წაიშალა", - "ScheduledTaskStartedWithName": "{0} გაეშვა", + "ScheduledTaskStartedWithName": "{0} დაიწყო", "VersionNumber": "ვერსია {0}", "TasksChannelsCategory": "ინტერნეტ-არხები", - "ValueSpecialEpisodeName": "სპეციალური - {0}", + "ValueSpecialEpisodeName": "დამატებითი - {0}", "TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.", "Channels": "არხები", "Collections": "კოლექციები", @@ -56,31 +56,31 @@ "Favorites": "რჩეულები", "Folders": "საქაღალდეები", "HeaderFavoriteShows": "რჩეული სერიალები", - "HeaderLiveTV": "ცოცხალი TV", - "HeaderNextUp": "შემდეგი ზემოთ", + "HeaderLiveTV": "ლაივ ტელევიზია", + "HeaderNextUp": "შემდეგი", "HomeVideos": "სახლის ვიდეოები", "NameSeasonNumber": "სეზონი {0}", "NameSeasonUnknown": "სეზონი უცნობია", - "NotificationOptionPluginError": "დამატების შეცდომა", - "NotificationOptionPluginInstalled": "დამატება დაყენებულია", - "NotificationOptionPluginUninstalled": "დამატება წაიშალა", + "NotificationOptionPluginError": "მოდულის შეცდომა", + "NotificationOptionPluginInstalled": "მოდული დაყენებულია", + "NotificationOptionPluginUninstalled": "მოდული წაიშალა", "ProviderValue": "მომწოდებელი: {0}", - "ScheduledTaskFailedWithName": "{0} ავარიულია", - "TvShows": "TV სერიალები", + "ScheduledTaskFailedWithName": "{0} ვერ შესრულდა", + "TvShows": "სატელევიზიო სერიალები", "TaskRefreshPeople": "ხალხის განახლება", - "TaskUpdatePlugins": "დამატებების განახლება", + "TaskUpdatePlugins": "მოდულების განახლება", "TaskRefreshChannels": "არხების განახლება", - "TaskOptimizeDatabase": "ბაზების ოპტიმიზაცია", + "TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია", "TaskKeyframeExtractor": "საკვანძო კადრის გამომღები", - "DeviceOnlineWithName": "{0} შეერთებულია", + "DeviceOnlineWithName": "{0} დაკავშირდა", "LabelIpAddressValue": "IP მისამართი: {0}", "NameInstallFailed": "{0}-ის დაყენების შეცდომა", "NotificationOptionApplicationUpdateAvailable": "ხელმისაწვდომია აპლიკაციის განახლება", "NotificationOptionAudioPlaybackStopped": "აუდიოს დაკვრა გაჩერებულია", "NotificationOptionNewLibraryContent": "ახალი შემცველობა დამატებულია", - "NotificationOptionPluginUpdateInstalled": "დამატების განახლება დაყენებულია", - "NotificationOptionServerRestartRequired": "სერვერის გადატვირთვა აუცილებელია", - "NotificationOptionTaskFailed": "დაგეგმილი ამოცანის შეცდომა", + "NotificationOptionPluginUpdateInstalled": "მოდულიs განახლება დაყენებულია", + "NotificationOptionServerRestartRequired": "საჭიროა სერვერის გადატვირთვა", + "NotificationOptionTaskFailed": "გეგმიური დავალების შეცდომა", "NotificationOptionUserLockedOut": "მომხმარებელი დაიბლოკა", "NotificationOptionVideoPlayback": "ვიდეოს დაკვრა დაწყებულია", "PluginInstalledWithName": "{0} დაყენებულია", @@ -91,39 +91,51 @@ "TaskRefreshLibrary": "მედიის ბიბლიოთეკის სკანირება", "TaskCleanLogs": "ჟურნალის საქაღალდის გასუფთავება", "TaskCleanTranscode": "ტრანსკოდირების საქაღალდის გასუფთავება", - "TaskDownloadMissingSubtitles": "ნაკლული სუბტიტრების გადმოწერა", - "UserDownloadingItemWithValues": "{0} -ი {0}-ს იწერს", - "FailedLoginAttemptWithUserName": "{0}-დან შემოსვლის შეცდომა", + "TaskDownloadMissingSubtitles": "მიუწვდომელი სუბტიტრების გადმოწერა", + "UserDownloadingItemWithValues": "{0} -ი {1}-ს იწერს", + "FailedLoginAttemptWithUserName": "შესვლის წარუმატებელი მცდელობა {0}-დან", "MessageApplicationUpdated": "Jellyfin-ის სერვერი განახლდა", "MessageServerConfigurationUpdated": "სერვერის კონფიგურაცია განახლდა", "ServerNameNeedsToBeRestarted": "საჭიროა {0}-ის გადატვირთვა", "UserCreatedWithName": "მომხმარებელი {0} შეიქმნა", "UserDeletedWithName": "მომხმარებელი {0} წაშლილია", - "UserOnlineFromDevice": "{0}-ი ხაზზეა {1}-დან", - "UserOfflineFromDevice": "{0}-ი {1}-დან გაითიშა", + "UserOnlineFromDevice": "{0}-ი დაკავშირდა {1}-დან", + "UserOfflineFromDevice": "{0}-ი {1}-დან გაეთიშა", "ItemAddedWithName": "{0} ჩამატებულია ბიბლიოთეკაში", "ItemRemovedWithName": "{0} წაშლილია ბიბლიოთეკიდან", "UserLockedOutWithName": "მომხმარებელი {0} დაბლოკილია", - "UserStartedPlayingItemWithValues": "{0} თამაშობს {1}-ს {2}-ზე", - "UserPasswordChangedWithName": "მომხმარებლისთვის {0} პაროლი შეცვლილია", + "UserStartedPlayingItemWithValues": "{0} უყურებს {1}-ს {2}-ზე", + "UserPasswordChangedWithName": "მომხმარებელი {0}-სთვის პაროლი შეიცვალა", "UserPolicyUpdatedWithName": "{0}-ის მომხმარებლის პოლიტიკა განახლდა", - "UserStoppedPlayingItemWithValues": "{0}-მა დაამთავრა {1}-ის დაკვრა {2}-ზე", + "UserStoppedPlayingItemWithValues": "{0}-მა დაასრულა {1}-ის ყურება {2}-ზე", "TaskRefreshChapterImagesDescription": "თავების მქონე ვიდეოებისთვის მინიატურების შექმნა.", "TaskKeyframeExtractorDescription": "უფრო ზუსტი HLS დასაკრავი სიებისითვის ვიდეოდან საკვანძო გადრების ამოღება. შეიძლება საკმაო დრო დასჭირდეს.", "NewVersionIsAvailable": "გადმოსაწერად ხელმისაწვდომია Jellyfin -ის ახალი ვერსია.", "CameraImageUploadedFrom": "ახალი კამერის გამოსახულება ატვირთულია {0}-დან", "StartupEmbyServerIsLoading": "Jellyfin სერვერი იტვირთება. მოგვიანებით სცადეთ.", - "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერის შეცდომა", + "SubtitleDownloadFailureFromForItem": "{0}-დან {1}-სთვის სუბტიტრების გადმოწერა ვერ შესრულდა", "ValueHasBeenAddedToLibrary": "{0} დაემატა თქვენს მედიის ბიბლიოთეკას", - "TaskCleanActivityLogDescription": "მითითებულ ასაკზე ძველი ჟურნალის ჩანაწერების წაშლა.", - "TaskCleanCacheDescription": "სისტემისთვის არასაჭირო ქეშის ფაილების წაშლა.", - "TaskRefreshLibraryDescription": "თქვენი მედია ბიბლიოთეკაში ახალი ფაილების ძებნა და მეტამონაცემების განახლება.", + "TaskCleanActivityLogDescription": "შლის მითითებულ ასაკზე ძველ ჟურნალის ჩანაწერებს.", + "TaskCleanCacheDescription": "შლის სისტემისთვის არასაჭირო ქეშის ფაილებს.", + "TaskRefreshLibraryDescription": "ეძებს ახალ ფაილებს თქვენს მედიის ბიბლიოთეკაში და ანახლებს მეტამონაცემებს.", "TaskCleanLogsDescription": "{0} დღეზე ძველი ჟურნალის ფაილების წაშლა.", "TaskRefreshPeopleDescription": "თქვენს მედიის ბიბლიოთეკაში მსახიობების და რეჟისორების მეტამონაცემების განახლება.", - "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", + "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული მოდულების განახლებების გადმოწერა და დაყენება.", "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", - "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", - "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.", - "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.", - "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება" + "TaskDownloadMissingSubtitlesDescription": "ეძებს ბიბლიოთეკაში მიუწვდომელ სუბტიტრებს ინტერნეტში მეტამონაცემებზე დაყრდნობით.", + "TaskOptimizeDatabaseDescription": "კუმშავს მონაცემთა ბაზას ადგილის გათავისუფლებლად. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.", + "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის დაშვებულ ბიბლიოთეკებში.", + "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება", + "TaskAudioNormalization": "აუდიოს ნორმალიზება", + "TaskAudioNormalizationDescription": "აანალიზებს ფაილებს აუდიოს ნორმალიზაციისთვის.", + "TaskDownloadMissingLyrics": "მიუწვდომელი ლირიკების ჩამოტვირთვა", + "TaskDownloadMissingLyricsDescription": "ჩამოტვირთავს ამჟამად ბიბლიოთეკაში არარსებულ ლირიკებს სიმღერებისთვის", + "TaskCleanCollectionsAndPlaylists": "კოლექციების და დასაკრავი სიების გასუფთავება", + "TaskCleanCollectionsAndPlaylistsDescription": "შლის არარსებულ ერთეულებს კოლექციებიდან და დასაკრავი სიებიდან.", + "TaskExtractMediaSegments": "მედია სეგმენტების სკანირება", + "TaskExtractMediaSegmentsDescription": "მედია სეგმენტების სკანირება მხარდაჭერილი მოდულებისთვის.", + "TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია", + "TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.", + "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება", + "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ." } From f788e8b741dbc0754d8274f95edd710fe460b0c2 Mon Sep 17 00:00:00 2001 From: Tushar Mhatre Date: Thu, 2 Apr 2026 01:49:27 -0400 Subject: [PATCH 63/94] Translated using Weblate (Hindi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hi/ --- Emby.Server.Implementations/Localization/Core/hi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 80db975ccb..6521ffab27 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -127,7 +127,7 @@ "TaskRefreshTrickplayImages": "ट्रिकप्लै चित्रों को सृजन करे", "TaskRefreshTrickplayImagesDescription": "नियत संग्रहों में चलचित्रों का ट्रीकप्लै दर्शनों को सृजन करे.", "TaskAudioNormalization": "श्रव्य सामान्यीकरण", - "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें", + "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें।", "TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ", "TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है", "TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन", @@ -136,5 +136,5 @@ "TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।", "TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।", "TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें", - "CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।" + "CleanupUserDataTask": "यूज़र डेटा सफाई कार्य" } From 397147d0352845afc278e1266cbb2e1b0a1f64b4 Mon Sep 17 00:00:00 2001 From: rimasx Date: Thu, 2 Apr 2026 07:50:31 -0400 Subject: [PATCH 64/94] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 91a0aa6639..21b27a28f2 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -133,8 +133,8 @@ "TaskDownloadMissingLyrics": "Hangi puuduvad laulusõnad", "TaskDownloadMissingLyricsDescription": "Laulusõnade allalaadimine", "TaskMoveTrickplayImagesDescription": "Liigutab trickplay pildid meediakogu sätete kohaselt.", - "TaskExtractMediaSegments": "Skaneeri meediasegmente", - "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.", + "TaskExtractMediaSegments": "Skaneeri meedialõike", + "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.", "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "CleanupUserDataTask": "Puhasta kasutajaandmed", "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud." From 9b00854e686c65ef4b0578071e5e2a4d9083181a Mon Sep 17 00:00:00 2001 From: HeroBrine1st Erquilenne Date: Fri, 5 Sep 2025 00:21:20 +0300 Subject: [PATCH 65/94] Add AlbumNormalizationGain field to BaseItemDto --- Emby.Server.Implementations/Dto/DtoService.cs | 9 +++++++++ MediaBrowser.Model/Dto/BaseItemDto.cs | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index b392340f71..08ced387b8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1019,6 +1019,15 @@ namespace Emby.Server.Implementations.Dto { dto.AlbumId = albumParent.Id; dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary); + if (albumParent.LUFS.HasValue) + { + // -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0 + dto.AlbumNormalizationGain = -18f - albumParent.LUFS; + } + else if (albumParent.NormalizationGain.HasValue) + { + dto.AlbumNormalizationGain = albumParent.NormalizationGain; + } } // if (options.ContainsField(ItemFields.MediaSourceCount)) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 8f223c12a5..e96bba0464 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -789,6 +789,12 @@ namespace MediaBrowser.Model.Dto /// The gain required for audio normalization. public float? NormalizationGain { get; set; } + /// + /// Gets or sets the gain required for audio normalization. This field is inherited from music album normalization gain. + /// + /// The gain required for audio normalization. + public float? AlbumNormalizationGain { get; set; } + /// /// Gets or sets the current program. /// From c5726559fd6b6a6263880e78737a268015836cf6 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 2 Apr 2026 19:43:53 -0400 Subject: [PATCH 66/94] Fix parental ratings not working on music albums --- MediaBrowser.Controller/Entities/Audio/MusicArtist.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 58841e5b78..c25694aba5 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -154,11 +154,6 @@ namespace MediaBrowser.Controller.Entities.Audio return "Artist-" + (Name ?? string.Empty).RemoveDiacritics(); } - protected override bool GetBlockUnratedValue(User user) - { - return user.GetPreferenceValues(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music); - } - public override UnratedItem GetBlockUnratedType() { return UnratedItem.Music; From 87c8349c6bcb5fe7b1c31cbaba2ef29b4b9a15f6 Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 3 Apr 2026 19:52:37 +0900 Subject: [PATCH 67/94] fix openapi report and publish workflows --- .github/workflows/openapi-merge.yml | 4 +++- .github/workflows/openapi-pull-request.yml | 18 +++++++++++++++--- .github/workflows/openapi-workflow-run.yml | 6 +++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/openapi-merge.yml b/.github/workflows/openapi-merge.yml index cd990cf5f8..954a835b41 100644 --- a/.github/workflows/openapi-merge.yml +++ b/.github/workflows/openapi-merge.yml @@ -6,7 +6,9 @@ on: tags: - 'v*' -permissions: {} +permissions: + contents: read + actions: read jobs: publish-openapi: diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index b583fb54d1..dc8ba3ab3e 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -63,10 +63,22 @@ jobs: name: openapi-base path: openapi-base - name: Detect Changes - uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0 + runs-on: ubuntu-latest id: openapi-diff with: old-spec: openapi-base/openapi.json new-spec: openapi-head/openapi.json - markdown: openapi-changelog.md - github-token: ${{ secrets.GITHUB_TOKEN }} + run: | + sed 's:allOf:oneOf:g' openapi-head/openapi.json + sed 's:allOf:oneOf:g' openapi-base/openapi.json + + mkdir -p /tmp/openapi-report + mv openapi-head/openapi.json /tmp/openapi-report/head.json + mv openapi-base/openapi.json /tmp/openapi-report/base.json + + docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md + - name: Upload Artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: openapi-report + path: /tmp/openapi-report/openapi-report.md diff --git a/.github/workflows/openapi-workflow-run.yml b/.github/workflows/openapi-workflow-run.yml index 9dbd2c40a0..0f9e84e56b 100644 --- a/.github/workflows/openapi-workflow-run.yml +++ b/.github/workflows/openapi-workflow-run.yml @@ -46,14 +46,14 @@ jobs: id: download_report uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: openapi-diff-report - path: openapi-diff-report + name: openapi-report + path: openapi-report run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Push Comment uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1 with: github-token: ${{ secrets.JF_BOT_TOKEN }} - file-path: ${{ steps.download_report.outputs.download-path }}/openapi-changelog.md + file-path: ${{ steps.download_report.outputs.download-path }}/openapi-report.md pr-number: ${{ needs.metadata.outputs.pr_number }} comment-tag: openapi-report From da4d06c5abf2fcbc1addcf64d456ee88ea100f3c Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 3 Apr 2026 20:17:31 +0900 Subject: [PATCH 68/94] move permissions block to publish job --- .github/workflows/openapi-merge.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/openapi-merge.yml b/.github/workflows/openapi-merge.yml index 954a835b41..2421c09ad7 100644 --- a/.github/workflows/openapi-merge.yml +++ b/.github/workflows/openapi-merge.yml @@ -6,14 +6,12 @@ on: tags: - 'v*' -permissions: - contents: read - actions: read - jobs: publish-openapi: name: OpenAPI - Publish Artifact uses: ./.github/workflows/openapi-generate.yml + permissions: + contents: read with: ref: ${{ github.sha }} repository: ${{ github.repository }} From 83ee0802004dc525c7977a44ad1cfccd8a8f6ff2 Mon Sep 17 00:00:00 2001 From: MrPlow Date: Fri, 3 Apr 2026 05:01:23 -0400 Subject: [PATCH 69/94] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- Emby.Server.Implementations/Localization/Core/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index e9a1630d9d..a102690e4d 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -19,7 +19,7 @@ "HeaderContinueWatching": "Weiterschauen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblingsinterpreten", - "HeaderFavoriteEpisodes": "Lieblingsepisoden", + "HeaderFavoriteEpisodes": "Lieblingsfolgen", "HeaderFavoriteShows": "Lieblingsserien", "HeaderFavoriteSongs": "Lieblingssongs", "HeaderLiveTV": "Live TV", From 12af9f9a5793b1491c04fd88dad521f1dda51767 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Fri, 3 Apr 2026 05:56:02 -0400 Subject: [PATCH 70/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 2e3fde2b04..ba94323094 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -16,7 +16,7 @@ "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯歌手", - "HeaderContinueWatching": "繼續觀看", + "HeaderContinueWatching": "繼續睇返", "HeaderFavoriteAlbums": "心水嘅專輯", "HeaderFavoriteArtists": "心水嘅藝人", "HeaderFavoriteEpisodes": "心水嘅劇集", From 6e81226054c30fd449c49b32792f7a26fe434363 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Fri, 3 Apr 2026 14:51:58 -0400 Subject: [PATCH 71/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index ba94323094..17cd2a9a41 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -9,8 +9,8 @@ "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", - "DeviceOfflineWithName": "{0} 斷開咗連接", - "DeviceOnlineWithName": "{0} 連接咗", + "DeviceOfflineWithName": "{0} 斷開咗連線", + "DeviceOnlineWithName": "{0} 連線咗", "FailedLoginAttemptWithUserName": "來自 {0} 嘅登入嘗試失敗咗", "Favorites": "心水", "Folders": "資料夾", From 94b3d41d7d229df5921d87eed66ab122a0516303 Mon Sep 17 00:00:00 2001 From: lednurb Date: Sat, 4 Apr 2026 02:48:03 -0400 Subject: [PATCH 72/94] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index dbbe2cbd08..ae5bd6ce9e 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -26,7 +26,7 @@ "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", - "Inherit": "Erven", + "Inherit": "Overnemen", "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek", "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek", "LabelIpAddressValue": "IP-adres: {0}", @@ -116,7 +116,7 @@ "TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.", "TaskCleanActivityLog": "Activiteitenlogboek legen", "Undefined": "Niet gedefinieerd", - "Forced": "Gedwongen", + "Forced": "Geforceerd", "Default": "Standaard", "TaskOptimizeDatabaseDescription": "Comprimeert de database en trimt vrije ruimte. Het uitvoeren van deze taak kan de prestaties verbeteren, na het scannen van de bibliotheek of andere aanpassingen die invloed hebben op de database.", "TaskOptimizeDatabase": "Database optimaliseren", From 5d2a529fb3f74429f69959363960e99b14427334 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 4 Apr 2026 17:39:15 +0900 Subject: [PATCH 73/94] fix invalid workflow on openapi report job --- .github/workflows/openapi-pull-request.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index dc8ba3ab3e..c7ecb7ecfb 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -63,11 +63,7 @@ jobs: name: openapi-base path: openapi-base - name: Detect Changes - runs-on: ubuntu-latest id: openapi-diff - with: - old-spec: openapi-base/openapi.json - new-spec: openapi-head/openapi.json run: | sed 's:allOf:oneOf:g' openapi-head/openapi.json sed 's:allOf:oneOf:g' openapi-base/openapi.json From 134fe92f42d348654bd6f895eb1016e104915a68 Mon Sep 17 00:00:00 2001 From: Kisnov Date: Sat, 4 Apr 2026 08:08:41 -0400 Subject: [PATCH 74/94] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 1e7279be83..fce3a614c2 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -63,8 +63,8 @@ "Photos": "Fotos", "Playlists": "Llistes de reproducció", "Plugin": "Complement", - "PluginInstalledWithName": "{0} ha estat instal·lat", - "PluginUninstalledWithName": "S'ha instal·lat {0}", + "PluginInstalledWithName": "{0} s'ha instal·lat", + "PluginUninstalledWithName": "{0} s'ha desinstal·lat", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", From 97a1feb16dfd694e00cd5ec75f582387c97120f9 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 5 Apr 2026 15:19:10 +0900 Subject: [PATCH 75/94] edit openapi files in place with sed --- .github/workflows/openapi-pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index c7ecb7ecfb..563a0a406f 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -65,8 +65,8 @@ jobs: - name: Detect Changes id: openapi-diff run: | - sed 's:allOf:oneOf:g' openapi-head/openapi.json - sed 's:allOf:oneOf:g' openapi-base/openapi.json + sed -i 's:allOf:oneOf:g' openapi-head/openapi.json + sed -i 's:allOf:oneOf:g' openapi-base/openapi.json mkdir -p /tmp/openapi-report mv openapi-head/openapi.json /tmp/openapi-report/head.json From 80df5dc984b714852987efb02700b46376452e5c Mon Sep 17 00:00:00 2001 From: dkanada Date: Thu, 12 Mar 2026 01:27:11 +0900 Subject: [PATCH 76/94] add StartIndex and ParentId to person search --- Jellyfin.Api/Controllers/PersonsController.cs | 6 ++++++ .../Item/PeopleRepository.cs | 11 ++++++++++- .../Entities/InternalPeopleQuery.cs | 4 ++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 438d054a4c..2b2afb0fe6 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -47,6 +47,7 @@ public class PersonsController : BaseJellyfinApiController /// /// Gets all persons. /// + /// Optional. All items with a lower index will be dropped from the response. /// Optional. The maximum number of records to return. /// The search term. /// Optional. Specify additional fields of information to return in the output. @@ -57,6 +58,7 @@ public class PersonsController : BaseJellyfinApiController /// Optional. The image types to include in the output. /// Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited. /// Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited. + /// Optional. Specify this to localize the search to a specific library. Omit to use the root. /// Optional. If specified, person results will be filtered on items related to said persons. /// User id. /// Optional, include image information in output. @@ -65,6 +67,7 @@ public class PersonsController : BaseJellyfinApiController [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetPersons( + [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, @@ -75,6 +78,7 @@ public class PersonsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] excludePersonTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] personTypes, + [FromQuery] Guid? parentId, [FromQuery] Guid? appearsInItemId, [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) @@ -96,6 +100,8 @@ public class PersonsController : BaseJellyfinApiController User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, + ParentId = parentId, + StartIndex = startIndex, Limit = limit ?? 0 }); diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index e2569241d2..ad9953d1b6 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -62,7 +62,11 @@ public class PeopleRepository(IDbContextFactory dbProvider, I using var context = _dbProvider.CreateDbContext(); var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct(); - // dbQuery = dbQuery.OrderBy(e => e.ListOrder); + if (filter.StartIndex.HasValue && filter.StartIndex > 0) + { + dbQuery = dbQuery.Skip(filter.StartIndex.Value); + } + if (filter.Limit > 0) { dbQuery = dbQuery.Take(filter.Limit); @@ -197,6 +201,11 @@ public class PeopleRepository(IDbContextFactory dbProvider, I query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.ItemId))); } + if (filter.ParentId != null) + { + query = query.Where(e => e.BaseItems!.Any(w => context.AncestorIds.Any(i => i.ParentItemId == filter.ParentId && i.ItemId == w.ItemId))); + } + if (!filter.AppearsInItemId.IsEmpty()) { query = query.Where(e => e.BaseItems!.Any(w => w.ItemId.Equals(filter.AppearsInItemId))); diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 203a16a668..f4b3910b0e 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -21,6 +21,8 @@ namespace MediaBrowser.Controller.Entities ExcludePersonTypes = excludePersonTypes; } + public int? StartIndex { get; set; } + /// /// Gets or sets the maximum number of items the query should return. /// @@ -28,6 +30,8 @@ namespace MediaBrowser.Controller.Entities public Guid ItemId { get; set; } + public Guid? ParentId { get; set; } + public IReadOnlyList PersonTypes { get; } public IReadOnlyList ExcludePersonTypes { get; } From cc7bfff41207a9756a00f73f22d8d6229b089ebd Mon Sep 17 00:00:00 2001 From: lednurb Date: Sun, 5 Apr 2026 03:30:13 -0400 Subject: [PATCH 77/94] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index ae5bd6ce9e..9b283e2a66 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,5 +1,5 @@ { - "Albums": "Albums", + "Albums": "", "AppDeviceValues": "App: {0}, Apparaat: {1}", "Application": "Applicatie", "Artists": "Artiesten", @@ -14,7 +14,7 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "Genres": "Genres", + "Genres": "", "HeaderAlbumArtists": "Albumartiesten", "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteAlbums": "Favoriete albums", From 3225023c1f8127832c60379154b8e3c82233f781 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Sun, 5 Apr 2026 02:18:43 -0400 Subject: [PATCH 78/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 17cd2a9a41..3000e7d59b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -79,7 +79,7 @@ "TvShows": "電視節目", "User": "使用者", "UserCreatedWithName": "經已建立咗新使用者 {0}", - "UserDeletedWithName": "使用者 {0} 經已被刪除", + "UserDeletedWithName": "使用者 {0} 經已被刪走", "UserDownloadingItemWithValues": "{0} 下載緊 {1}", "UserLockedOutWithName": "使用者 {0} 經已被鎖定", "UserOfflineFromDevice": "{0} 經已由 {1} 斷開咗連線", @@ -99,16 +99,16 @@ "TaskDownloadMissingSubtitlesDescription": "根據媒體詳細資料設定,喺網上幫你搵返啲欠缺嘅字幕。", "TaskRefreshChannelsDescription": "重新整理網上頻道嘅資訊。", "TaskRefreshChannels": "重新載入頻道", - "TaskCleanTranscodeDescription": "自動刪除超過一日嘅轉碼檔案。", + "TaskCleanTranscodeDescription": "自動刪走超過一日嘅轉碼檔案。", "TaskCleanTranscode": "清理轉碼資料夾", "TaskUpdatePluginsDescription": "自動幫嗰啲設咗要自動更新嘅外掛程式進行下載同安裝。", "TaskRefreshPeopleDescription": "更新媒體櫃入面演員同導演嘅媒體詳細資料。", - "TaskCleanLogsDescription": "自動刪除超過 {0} 日嘅紀錄檔。", + "TaskCleanLogsDescription": "自動刪走超過 {0} 日嘅紀錄檔。", "TaskCleanLogs": "清理日誌資料夾", "TaskRefreshLibrary": "掃描媒體櫃", "TaskRefreshChapterImagesDescription": "幫有章節嘅影片整返啲章節縮圖。", "TaskRefreshChapterImages": "擷取章節圖片", - "TaskCleanCacheDescription": "刪除系統已經唔再需要嘅快取檔案。", + "TaskCleanCacheDescription": "刪走系統已經唔再需要嘅快取檔案。", "TaskCleanCache": "清理快取(Cache)資料夾", "TasksChannelsCategory": "網路頻道", "TasksLibraryCategory": "媒體櫃", @@ -119,7 +119,7 @@ "Default": "初始", "TaskOptimizeDatabaseDescription": "壓縮數據櫃並釋放剩餘空間。喺掃描媒體櫃或者做咗一啲會修改數據櫃嘅操作之後行呢個任務,或者可以提升效能。", "TaskOptimizeDatabase": "最佳化數據櫃", - "TaskCleanActivityLogDescription": "刪除超過設定日期嘅活動記錄。", + "TaskCleanActivityLogDescription": "刪走超過設定日期嘅活動記錄。", "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)嚟建立更準確嘅 HLS 播放列表。呢個任務可能要行好耐。", "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", From 7f3e27c0075bdbcad6ebd4dd77184cb9ce99b6e0 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 5 Apr 2026 05:36:43 -0400 Subject: [PATCH 79/94] Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ --- Emby.Server.Implementations/Localization/Core/nl.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 9b283e2a66..e78b0ebabe 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,5 +1,4 @@ { - "Albums": "", "AppDeviceValues": "App: {0}, Apparaat: {1}", "Application": "Applicatie", "Artists": "Artiesten", @@ -14,7 +13,6 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "Genres": "", "HeaderAlbumArtists": "Albumartiesten", "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteAlbums": "Favoriete albums", From 8482b3cfb9f5993fee35743da030b2bd1c8a7fc0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:03:00 +0000 Subject: [PATCH 80/94] Update dependency z440.atl.core to 7.12.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3385ee070a..f15f7c7a75 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ - + From 34e2f91d502fc9034591836947e4351d44aa9f12 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 5 Apr 2026 16:35:15 -0400 Subject: [PATCH 81/94] Update issue template version to 10.11.8 --- .github/ISSUE_TEMPLATE/issue report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 909f22ed1d..45235be712 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,6 +87,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: + - 10.11.8 - 10.11.7 - 10.11.6 - Master From 31720cef053a4acce332025ff2aa83bd1bb56b9c Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Sun, 5 Apr 2026 12:34:45 -0400 Subject: [PATCH 82/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- .../Localization/Core/zh-HK.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 3000e7d59b..68773599ce 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -5,7 +5,7 @@ "Artists": "藝人", "AuthenticationSucceededWithUserName": "{0} 成功通過驗證", "Books": "書籍", - "CameraImageUploadedFrom": "{0} 已經成功上傳咗一張新相", + "CameraImageUploadedFrom": "{0} 已經成功上載咗一張新相", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "心水嘅節目", "HeaderFavoriteSongs": "心水嘅歌曲", "HeaderLiveTV": "電視直播", - "HeaderNextUp": "繼續觀看", + "HeaderNextUp": "跟住落嚟", "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", @@ -48,7 +48,7 @@ "NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗", "NotificationOptionAudioPlayback": "開始播放音訊", "NotificationOptionAudioPlaybackStopped": "停咗播放音訊", - "NotificationOptionCameraImageUploaded": "相機相片上傳咗", + "NotificationOptionCameraImageUploaded": "相機相片上載咗", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "加咗新內容", "NotificationOptionPluginError": "外掛程式錯誤", @@ -72,7 +72,7 @@ "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動", "Shows": "節目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,請稍後再試。", + "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入緊,唔該稍後再試。", "SubtitleDownloadFailureFromForItem": "經 {0} 下載 {1} 嘅字幕失敗咗", "Sync": "同步", "System": "系統", @@ -89,7 +89,7 @@ "UserStartedPlayingItemWithValues": "{0} 正喺 {2} 播緊 {1}", "UserStoppedPlayingItemWithValues": "{0} 已經喺 {2} 停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已經成功加入咗你嘅媒體櫃", - "ValueSpecialEpisodeName": "特別篇 - {0}", + "ValueSpecialEpisodeName": "特輯 - {0}", "VersionNumber": "版本 {0}", "TaskDownloadMissingSubtitles": "下載漏咗嘅字幕", "TaskUpdatePlugins": "更新外掛程式", From 76c17856ba70024b66142d9634075c8494405e22 Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:34:00 -0400 Subject: [PATCH 83/94] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index e78b0ebabe..76950467bd 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -135,5 +135,7 @@ "TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.", "TaskExtractMediaSegments": "Scannen op mediasegmenten", "CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig zijn.", - "CleanupUserDataTask": "Opruimtaak gebruikersdata" + "CleanupUserDataTask": "Opruimtaak gebruikersdata", + "Albums": "Albums", + "Genres": "Genres" } From f5e9c1de45f8bc231ba3a5b4636db887334c0b96 Mon Sep 17 00:00:00 2001 From: kscop-n1 Date: Mon, 6 Apr 2026 04:49:56 -0400 Subject: [PATCH 84/94] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/ --- Emby.Server.Implementations/Localization/Core/uk.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 3ad772aa9c..6d347322f3 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -124,8 +124,8 @@ "TaskKeyframeExtractor": "Екстрактор ключових кадрів", "External": "Зовнішній", "HearingImpaired": "З порушеннями слуху", - "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.", - "TaskRefreshTrickplayImages": "Створити Trickplay-зображення", + "TaskRefreshTrickplayImagesDescription": "Створює прев'ю-зображення для відео у ввімкнених медіатеках.", + "TaskRefreshTrickplayImages": "Створити Прев'ю-зображення", "TaskCleanCollectionsAndPlaylists": "Очистити колекції і списки відтворення", "TaskCleanCollectionsAndPlaylistsDescription": "Видаляє елементи з колекцій і списків відтворення, які більше не існують.", "TaskAudioNormalizationDescription": "Сканує файли на наявність даних для нормалізації звуку.", @@ -134,7 +134,7 @@ "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень", "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", "TaskExtractMediaSegments": "Сканування медіа-сегментів", - "TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень", + "TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень", "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.", "CleanupUserDataTask": "Завдання очищення даних користувача", "CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому." From 83c9ab007914c2b56f861a00b1864f90abfab369 Mon Sep 17 00:00:00 2001 From: kscop-n1 Date: Mon, 6 Apr 2026 04:50:01 -0400 Subject: [PATCH 85/94] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uk/ --- Emby.Server.Implementations/Localization/Core/uk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index 6d347322f3..26f49573e7 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -132,7 +132,7 @@ "TaskAudioNormalization": "Нормалізація аудіо", "TaskDownloadMissingLyrics": "Завантажити відсутні тексти пісень", "TaskDownloadMissingLyricsDescription": "Завантаження текстів пісень", - "TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.", + "TaskMoveTrickplayImagesDescription": "Переміщує наявні прев'ю-зображення відповідно до налаштувань медіатеки.", "TaskExtractMediaSegments": "Сканування медіа-сегментів", "TaskMoveTrickplayImages": "Змінити місце розташування прев'ю-зображень", "TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.", From 142ba42883f7713b7fcd9a82b170f86b2c933c4b Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:19:32 -0400 Subject: [PATCH 86/94] Backport pull request #16539 from jellyfin/release-10.11.z Fix subtitle saving Original-merge: f51c63e244436944d5269085a1bed1e56db7a78e Merged-by: nielsvanvelzen Backported-by: Bond_009 --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index ae5e1090ad..420dd39a48 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -221,6 +221,11 @@ namespace MediaBrowser.Providers.Subtitles private async Task TrySaveToFiles(Stream stream, List savePaths, Video video, string extension) { + if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Invalid subtitle format: {extension}"); + } + List? exs = null; foreach (var savePath in savePaths) From c008f28d3126186e0a646121a3f69bd1624e37f5 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 6 Apr 2026 05:19:33 -0400 Subject: [PATCH 87/94] Backport pull request #16540 from jellyfin/release-10.11.z Handle folders without associated library in FixLibrarySubtitleDownloadLanguages Original-merge: be095f85ab80db1d20fccba8774856abe9ae0bd1 Merged-by: nielsvanvelzen Backported-by: Bond_009 --- .../Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs index e82123e5ac..2b1f549940 100644 --- a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs +++ b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs @@ -7,7 +7,6 @@ using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -50,7 +49,7 @@ internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine foreach (var virtualFolder in virtualFolders) { var options = virtualFolder.LibraryOptions; - if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) + if (options?.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) { continue; } From 8cecf53057b112a5b169d04e3994d1fb233e22f3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 29 Mar 2026 17:22:14 -0400 Subject: [PATCH 88/94] Fix GHSA-j2hf-x4q5-47j3 with improved sanitization Co-Authored-By: Shadowghost --- MediaBrowser.Controller/Entities/BaseItem.cs | 15 +++++++++---- .../MediaInfo/ProbeProvider.cs | 21 ++++++++++++++++++- .../Subtitles/SubtitleManager.cs | 18 +++++++++++++--- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8f89c1c797..e312e9d80b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1171,11 +1171,18 @@ namespace MediaBrowser.Controller.Entities info.Video3DFormat = video.Video3DFormat; info.Timestamp = video.Timestamp; - if (video.IsShortcut) + if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath)) { - info.IsRemote = true; - info.Path = video.ShortcutPath; - info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); + var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath); + + // Only allow remote shortcut paths — local file paths in .strm files + // could be used to read arbitrary files from the server. + if (shortcutProtocol != MediaProtocol.File) + { + info.IsRemote = true; + info.Path = video.ShortcutPath; + info.Protocol = shortcutProtocol; + } } if (string.IsNullOrEmpty(info.Container)) diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 9f5463b82c..c3ff26202f 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -262,9 +262,28 @@ namespace MediaBrowser.Providers.MediaInfo private void FetchShortcutInfo(BaseItem item) { - item.ShortcutPath = File.ReadAllLines(item.Path) + var shortcutPath = File.ReadAllLines(item.Path) .Select(NormalizeStrmLine) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); + + if (string.IsNullOrWhiteSpace(shortcutPath)) + { + return; + } + + // Only allow remote URLs in .strm files to prevent local file access + if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase))) + { + item.ShortcutPath = shortcutPath; + } + else + { + _logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath); + } } /// diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 420dd39a48..9f95a9d959 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Emby.Naming.Common; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; @@ -32,6 +33,7 @@ namespace MediaBrowser.Providers.Subtitles private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; private readonly ILocalizationManager _localization; + private readonly HashSet _allowedSubtitleFormats; private readonly ISubtitleProvider[] _subtitleProviders; @@ -41,7 +43,8 @@ namespace MediaBrowser.Providers.Subtitles ILibraryMonitor monitor, IMediaSourceManager mediaSourceManager, ILocalizationManager localizationManager, - IEnumerable subtitleProviders) + IEnumerable subtitleProviders, + NamingOptions namingOptions) { _logger = logger; _fileSystem = fileSystem; @@ -51,6 +54,9 @@ namespace MediaBrowser.Providers.Subtitles _subtitleProviders = subtitleProviders .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) .ToArray(); + _allowedSubtitleFormats = new HashSet( + namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')), + StringComparer.OrdinalIgnoreCase); } /// @@ -171,6 +177,12 @@ namespace MediaBrowser.Providers.Subtitles /// public Task UploadSubtitle(Video video, SubtitleResponse response) { + var format = response.Format; + if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format)) + { + throw new ArgumentException($"Unsupported subtitle format: '{format}'"); + } + var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); return TrySaveSubtitle(video, libraryOptions, response); } @@ -230,7 +242,7 @@ namespace MediaBrowser.Providers.Subtitles foreach (var savePath in savePaths) { - var path = savePath + "." + extension; + var path = Path.GetFullPath(savePath + "." + extension); try { if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal) @@ -241,7 +253,7 @@ namespace MediaBrowser.Providers.Subtitles while (fileExists) { - path = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension); + path = Path.GetFullPath(string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension)); fileExists = File.Exists(path); counter++; } From 3c9b71e1241237107c260bb84b9221f532ef8105 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 29 Mar 2026 17:30:09 -0400 Subject: [PATCH 89/94] Fix GHSA-8fw7-f233-ffr8 with improved sanitization Co-Authored-By: Shadowghost --- Jellyfin.Data/UserEntityExtensions.cs | 2 +- src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs index 149fc9042d..0fc8d3cd25 100644 --- a/Jellyfin.Data/UserEntityExtensions.cs +++ b/Jellyfin.Data/UserEntityExtensions.cs @@ -185,7 +185,7 @@ public static class UserEntityExtensions entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); - entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false)); entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index 2270758454..5da7762f6f 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts } else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { + if (!IsValidChannelUrl(trimmedLine)) + { + _logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine); + extInf = string.Empty; + continue; + } + var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine); channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); @@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts return numberString; } + private static bool IsValidChannelUrl(string url) + { + return Uri.TryCreate(url, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase)); + } + private static bool IsValidChannelNumber(string numberString) { if (string.IsNullOrWhiteSpace(numberString) From 3c2833e3e8945a119dfd59f53a7bb76ac51c566c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 30 Mar 2026 09:40:01 +0200 Subject: [PATCH 90/94] Fix GHSA v2jv-54xj-h76w --- Jellyfin.Api/Controllers/SyncPlayController.cs | 2 +- Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 3d6874079d..991fb87144 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController [FromBody, Required] NewGroupRequestDto requestData) { var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new NewGroupRequest(requestData.GroupName); + var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim()); return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None)); } diff --git a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs index 32a3bb444c..2e1889fed4 100644 --- a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs +++ b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace Jellyfin.Api.Models.SyncPlayDtos; /// @@ -17,5 +19,6 @@ public class NewGroupRequestDto /// Gets or sets the group name. /// /// The name of the new group. + [StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")] public string GroupName { get; set; } } From b846958f2c99271ff68de1cc6b252b5c851fb01c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 30 Mar 2026 10:48:51 +0200 Subject: [PATCH 91/94] Add additional validations --- Jellyfin.Api/Controllers/AudioController.cs | 20 +++--- .../Controllers/DynamicHlsController.cs | 62 +++++++++---------- Jellyfin.Api/Controllers/LiveTvController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 4 +- Jellyfin.Api/Controllers/VideosController.cs | 20 +++--- Jellyfin.Api/Helpers/StreamingHelpers.cs | 21 +++++-- .../MediaEncoding/EncodingHelper.cs | 21 ++++--- .../Subtitles/SubtitleManager.cs | 16 +++-- 8 files changed, 97 insertions(+), 69 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 4be79ff5a0..b6b3e8fe95 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index acd5dd64ec..2044710978 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -206,8 +206,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -412,12 +412,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -448,8 +448,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -585,12 +585,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -620,8 +620,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -752,12 +752,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -788,8 +788,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -921,12 +921,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -956,8 +956,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1091,7 +1091,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1099,12 +1099,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1135,8 +1135,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1273,7 +1273,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1281,12 +1281,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1316,8 +1316,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 94f62a0713..3600a79621 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1185,7 +1185,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesVideoFile] public ActionResult GetLiveStreamFile( [FromRoute, Required] string streamId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index b1a91ae70f..f4e0c86143 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -101,13 +101,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index ccf8e90632..afae756e48 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -313,18 +313,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -355,8 +355,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -551,18 +551,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -593,8 +593,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index c6823fa807..047d4ed867 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -422,14 +422,18 @@ public static class StreamingHelpers request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); break; case 4: - if (videoRequest is not null) + if (videoRequest is not null && IsValidCodecName(val)) { videoRequest.VideoCodec = val; } break; case 5: - request.AudioCodec = val; + if (IsValidCodecName(val)) + { + request.AudioCodec = val; + } + break; case 6: if (videoRequest is not null) @@ -504,7 +508,7 @@ public static class StreamingHelpers break; case 18: - if (videoRequest is not null) + if (videoRequest is not null && IsValidCodecName(val)) { videoRequest.Profile = val; } @@ -563,7 +567,11 @@ public static class StreamingHelpers break; case 30: - request.SubtitleCodec = val; + if (IsValidCodecName(val)) + { + request.SubtitleCodec = val; + } + break; case 31: if (videoRequest is not null) @@ -586,6 +594,11 @@ public static class StreamingHelpers } } + private static bool IsValidCodecName(string val) + { + return EncodingHelper.ContainerValidationRegex().IsMatch(val); + } + /// /// Parses the container into its file extension. /// diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index f2468782ff..559f763ada 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -33,12 +33,12 @@ namespace MediaBrowser.Controller.MediaEncoding public partial class EncodingHelper { /// - /// The codec validation regex. + /// The codec validation regex string. /// This regular expression matches strings that consist of alphanumeric characters, hyphens, /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. /// This should matches all common valid codecs. /// - public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; /// /// The level validation regex. @@ -87,8 +87,6 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0); - private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); - private static readonly string[] _videoProfilesH264 = [ "ConstrainedBaseline", @@ -181,6 +179,15 @@ namespace MediaBrowser.Controller.MediaEncoding RemoveHdr10Plus, } + /// + /// The codec validation regex. + /// This regular expression matches strings that consist of alphanumeric characters, hyphens, + /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. + /// This should matches all common valid codecs. + /// + [GeneratedRegex(@"^[a-zA-Z0-9\-\._,|]{0,40}$")] + public static partial Regex ContainerValidationRegex(); + [GeneratedRegex(@"\s+")] private static partial Regex WhiteSpaceRegex(); @@ -477,7 +484,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetMjpegEncoder(state, encodingOptions); } - if (_containerValidationRegex.IsMatch(codec)) + if (ContainerValidationRegex().IsMatch(codec)) { return codec.ToLowerInvariant(); } @@ -518,7 +525,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetInputFormat(string container) { - if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container)) + if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container)) { return null; } @@ -736,7 +743,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = state.OutputAudioCodec; - if (!_containerValidationRegex.IsMatch(codec)) + if (!ContainerValidationRegex().IsMatch(codec)) { codec = "aac"; } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 9f95a9d959..a78ec995cf 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -205,7 +205,13 @@ namespace MediaBrowser.Providers.Subtitles } var savePaths = new List(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + var language = response.Language.ToLowerInvariant(); + if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0) + { + throw new ArgumentException("Language contains invalid characters."); + } + + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language; if (response.IsForced) { @@ -245,15 +251,17 @@ namespace MediaBrowser.Providers.Subtitles var path = Path.GetFullPath(savePath + "." + extension); try { - if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal) - || path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar; + var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar; + if (path.StartsWith(containingFolder, StringComparison.Ordinal) + || path.StartsWith(metadataFolder, StringComparison.Ordinal)) { var fileExists = File.Exists(path); var counter = 0; while (fileExists) { - path = Path.GetFullPath(string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension)); + path = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension); fileExists = File.Exists(path); counter++; } From 0bf7653e3648a86bd0c6f224fcea341a6fdb8a85 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 31 Mar 2026 09:30:45 +0200 Subject: [PATCH 92/94] Fix GHSA-jh22-fw8w-2v9x --- Jellyfin.Api/Controllers/AudioController.cs | 4 +- .../Controllers/DynamicHlsController.cs | 14 ++-- Jellyfin.Api/Controllers/VideosController.cs | 4 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 +- .../MediaEncoding/EncodingHelper.cs | 71 ++++++++++--------- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index b6b3e8fe95..590bd05da4 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 2044710978..c13da3ac7b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -187,7 +187,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -427,7 +427,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -601,7 +601,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -767,7 +767,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -937,7 +937,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1114,7 +1114,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1297,7 +1297,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index afae756e48..7854edc5ac 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -334,7 +334,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -572,7 +572,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 047d4ed867..bae2756303 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -17,9 +17,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers; @@ -487,7 +485,7 @@ public static class StreamingHelpers request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); break; case 15: - if (videoRequest is not null) + if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val)) { videoRequest.Level = val; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 559f763ada..9f7e35d1ea 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -41,10 +41,10 @@ namespace MediaBrowser.Controller.MediaEncoding public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; /// - /// The level validation regex. + /// The level validation regex string. /// This regular expression matches strings representing a double. /// - public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?"; + public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?"; private const string _defaultMjpegEncoder = "mjpeg"; @@ -185,9 +185,16 @@ namespace MediaBrowser.Controller.MediaEncoding /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. /// This should matches all common valid codecs. /// - [GeneratedRegex(@"^[a-zA-Z0-9\-\._,|]{0,40}$")] + [GeneratedRegex(ContainerValidationRegexStr)] public static partial Regex ContainerValidationRegex(); + /// + /// The level validation regex string. + /// This regular expression matches strings representing a double. + /// + [GeneratedRegex(LevelValidationRegexStr)] + public static partial Regex LevelValidationRegex(); + [GeneratedRegex(@"\s+")] private static partial Regex WhiteSpaceRegex(); @@ -1797,38 +1804,40 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) + if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { - if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + return null; + } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.3 (15) and lower for maximum compatibility. + // https://en.wikipedia.org/wiki/AV1#Levels + if (requestLevel < 0 || requestLevel >= 15) { - // Transcode to level 5.3 (15) and lower for maximum compatibility. - // https://en.wikipedia.org/wiki/AV1#Levels - if (requestLevel < 0 || requestLevel >= 15) - { - return "15"; - } + return "15"; } - else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + } + else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.0 and lower for maximum compatibility. + // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. + // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels + // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. + if (requestLevel < 0 || requestLevel >= 150) { - // Transcode to level 5.0 and lower for maximum compatibility. - // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. - // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels - // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. - if (requestLevel < 0 || requestLevel >= 150) - { - return "150"; - } + return "150"; } - else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + } + else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.1 and lower for maximum compatibility. + // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. + // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels + if (requestLevel < 0 || requestLevel >= 51) { - // Transcode to level 5.1 and lower for maximum compatibility. - // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. - // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels - if (requestLevel < 0 || requestLevel >= 51) - { - return "51"; - } + return "51"; } } @@ -2218,12 +2227,10 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var level = state.GetRequestedLevel(targetVideoCodec); + var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec)); if (!string.IsNullOrEmpty(level)) { - level = NormalizeTranscodingLevel(state, level); - // libx264, QSV, AMF can adjust the given level to match the output. if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) From 740e9f8749ccf54afe8c0c2b1ff39a9775ed305b Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 31 Mar 2026 16:35:15 +0200 Subject: [PATCH 93/94] Lock down tuner API to be admin-only --- Jellyfin.Api/Controllers/LiveTvController.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 3600a79621..9a32a303a9 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -454,7 +454,7 @@ public class LiveTvController : BaseJellyfinApiController /// A . [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] public async Task ResetTuner([FromRoute, Required] string tunerId) { await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); @@ -976,7 +976,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created tuner host returned. /// A containing the created tuner host. [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); @@ -988,7 +988,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host deleted. /// A . [HttpDelete("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { @@ -1021,7 +1021,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created listings provider returned. /// A containing the created listings provider. [HttpPost("ListingProviders")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task> AddListingProvider( @@ -1047,7 +1047,7 @@ public class LiveTvController : BaseJellyfinApiController /// Listing provider deleted. /// A . [HttpDelete("ListingProviders")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { @@ -1080,7 +1080,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available countries returned. /// A containing the available countries. [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() @@ -1101,7 +1101,7 @@ public class LiveTvController : BaseJellyfinApiController /// Channel mapping options returned. /// An containing the channel mapping options. [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task GetChannelMappingOptions([FromQuery] string? providerId) => _listingsManager.GetChannelMappingOptions(providerId); @@ -1113,7 +1113,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created channel mapping returned. /// An containing the created channel mapping. [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task SetChannelMapping([FromBody, Required] SetChannelMappingDto dto) => _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId); @@ -1137,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the tuners. [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public IAsyncEnumerable DiscoverTuners([FromQuery] bool newDevicesOnly = false) => _tunerHostManager.DiscoverTuners(newDevicesOnly); @@ -1185,7 +1185,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesVideoFile] public ActionResult GetLiveStreamFile( [FromRoute, Required] string streamId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container) + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) From b28a5794ec2947bd8333be871c3b5ddeeedbc9d4 Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Mon, 6 Apr 2026 15:34:18 -0400 Subject: [PATCH 94/94] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 68773599ce..8ae899a73c 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -42,7 +42,7 @@ "MusicVideos": "MV", "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", - "NameSeasonUnknown": "未知的季度", + "NameSeasonUnknown": "未知嘅季度", "NewVersionIsAvailable": "有新版本嘅 Jellyfin 可以下載。", "NotificationOptionApplicationUpdateAvailable": "有得更新應用程式", "NotificationOptionApplicationUpdateInstalled": "應用程式更新好咗",