From 93902fc610a9d8b52780d88f7bb986e668567c9d Mon Sep 17 00:00:00 2001
From: john janzen
Date: Sat, 20 Dec 2025 19:42:51 +0100
Subject: [PATCH 001/136] 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 002/136] 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 003/136] 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 004/136] 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 77ff451e60fa1fee31da1f5987e6e47a22244bb8 Mon Sep 17 00:00:00 2001
From: Shadowghost
Date: Sun, 1 Feb 2026 21:19:20 +0100
Subject: [PATCH 005/136] Only save unique values of ProductionLocations,
Studios, Tags, Artists and AlbumArtists
---
.../Controllers/ItemUpdateController.cs | 10 +++----
.../Item/BaseItemRepository.cs | 27 ++++++++++---------
2 files changed, 19 insertions(+), 18 deletions(-)
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 605d2aeec2..b127433962 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -270,7 +270,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.Studios is not null)
{
- item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
+ item.Studios = Array.ConvertAll(request.Studios, x => x.Name).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
if (request.DateCreated.HasValue)
@@ -287,7 +287,7 @@ public class ItemUpdateController : BaseJellyfinApiController
item.CustomRating = request.CustomRating;
var currentTags = item.Tags;
- var newTags = request.Tags;
+ var newTags = request.Tags.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
var removedTags = currentTags.Except(newTags).ToList();
var addedTags = newTags.Except(currentTags).ToList();
item.Tags = newTags;
@@ -373,7 +373,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.ProductionLocations is not null)
{
- item.ProductionLocations = request.ProductionLocations;
+ item.ProductionLocations = request.ProductionLocations.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
@@ -421,7 +421,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
- hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
+ hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
}
@@ -429,7 +429,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
- hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
+ hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
}
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 5bb4494dd2..cd28c6e43e 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -683,14 +683,15 @@ public sealed class BaseItemRepository
.SelectMany(f => f.Values)
.Distinct()
.ToArray();
+
+ var types = allListedItemValues.Select(e => e.MagicNumber).Distinct().ToArray();
+ var values = allListedItemValues.Select(e => e.Value).Distinct().ToArray();
+ var allListedItemValuesSet = allListedItemValues.ToHashSet();
+
var existingValues = context.ItemValues
- .Select(e => new
- {
- item = e,
- Key = e.Type + "+" + e.Value
- })
- .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
- .Select(e => e.item)
+ .Where(e => types.Contains(e.Type) && values.Contains(e.Value))
+ .AsEnumerable()
+ .Where(e => allListedItemValuesSet.Contains((e.Type, e.Value)))
.ToArray();
var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
{
@@ -1050,7 +1051,7 @@ public sealed class BaseItemRepository
entity.TotalBitrate = dto.TotalBitrate;
entity.ExternalId = dto.ExternalId;
entity.Size = dto.Size;
- entity.Genres = string.Join('|', dto.Genres);
+ entity.Genres = string.Join('|', dto.Genres.Distinct(StringComparer.OrdinalIgnoreCase));
entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
entity.ChannelId = dto.ChannelId;
@@ -1077,9 +1078,9 @@ public sealed class BaseItemRepository
}
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
- entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
- entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
- entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
+ entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p)).Distinct(StringComparer.OrdinalIgnoreCase)) : null;
+ entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
+ entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
.Select(e => new BaseItemMetadataField()
{
@@ -1122,12 +1123,12 @@ public sealed class BaseItemRepository
if (dto is IHasArtist hasArtists)
{
- entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists) : null;
+ entity.Artists = hasArtists.Artists is not null ? string.Join('|', hasArtists.Artists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
}
if (dto is IHasAlbumArtist hasAlbumArtists)
{
- entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists) : null;
+ entity.AlbumArtists = hasAlbumArtists.AlbumArtists is not null ? string.Join('|', hasAlbumArtists.AlbumArtists.Distinct(StringComparer.OrdinalIgnoreCase)) : null;
}
if (dto is LiveTvProgram program)
From 1dacb69d80d0fd65f8c45dd54ab278bc8fb28ed1 Mon Sep 17 00:00:00 2001
From: Shadowghost
Date: Sun, 1 Feb 2026 21:51:36 +0100
Subject: [PATCH 006/136] Fix Genre Uniqueness
---
Jellyfin.Api/Controllers/ItemUpdateController.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index b127433962..4faec060d8 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -249,7 +249,7 @@ public class ItemUpdateController : BaseJellyfinApiController
item.IndexNumber = request.IndexNumber;
item.ParentIndexNumber = request.ParentIndexNumber;
item.Overview = request.Overview;
- item.Genres = request.Genres;
+ item.Genres = request.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
if (item is Episode episode)
{
From 1c2f08bc173fca586484ece49326d477622ac0bf Mon Sep 17 00:00:00 2001
From: tyage
Date: Thu, 12 Feb 2026 00:23:44 +0900
Subject: [PATCH 007/136] 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 106f33227a20f1785b7a6c59ef6d4b68a5a8a880 Mon Sep 17 00:00:00 2001
From: theguymadmax
Date: Sat, 14 Feb 2026 00:32:07 -0500
Subject: [PATCH 008/136] Use artist images for music library thumbnail
---
.../Images/CollectionFolderImageProvider.cs | 2 +-
MediaBrowser.Controller/Entities/BaseItem.cs | 12 ------------
2 files changed, 1 insertion(+), 13 deletions(-)
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index a25373326f..095934f896 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Images
includeItemTypes = new[] { BaseItemKind.Series };
break;
case CollectionType.music:
- includeItemTypes = new[] { BaseItemKind.MusicAlbum };
+ includeItemTypes = new[] { BaseItemKind.MusicArtist }; // Music albums usually don't have dedicated backdrops, so use artist instead
break;
case CollectionType.musicvideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo };
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 7586b99e77..cb38b61119 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -22,7 +22,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
@@ -2129,17 +2128,6 @@ namespace MediaBrowser.Controller.Entities
};
}
- // Music albums usually don't have dedicated backdrops, so return one from the artist instead
- if (GetType() == typeof(MusicAlbum) && imageType == ImageType.Backdrop)
- {
- var artist = FindParent();
-
- if (artist is not null)
- {
- return artist.GetImages(imageType).ElementAtOrDefault(imageIndex);
- }
- }
-
return GetImages(imageType)
.ElementAtOrDefault(imageIndex);
}
From 37b50fe13c689e8fb89288da12f79bf7164e1194 Mon Sep 17 00:00:00 2001
From: Cosmin Dumitru
Date: Wed, 18 Feb 2026 21:08:35 +0100
Subject: [PATCH 009/136] Fix malformed query string in StreamInfo.ToUrl()
causing 500 error via proxies
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
StreamInfo.ToUrl() generated URLs like `/master.m3u8?&DeviceId=...` (note `?&`)
because `?` was appended to the path and all parameters started with `&`. When
the first optional parameter (DeviceProfileId) was null, the result was a
malformed query string.
This is harmless when clients hit Jellyfin directly (ASP.NET Core tolerates `?&`),
but when accessed through a reverse proxy that parses and re-serializes the URL
(e.g. Home Assistant ingress via aiohttp/yarl), `?&` becomes `?=&` — introducing
an empty-key query parameter. ParseStreamOptions then crashes on `param.Key[0]`
with IndexOutOfRangeException.
Changes:
- StreamInfo.ToUrl(): Track query start position and replace the first `&` with
`?` after all parameters are appended, producing valid query strings
- ParseStreamOptions: Guard against empty query parameter keys
- Tests: Remove .Replace("?&", "?") workaround that masked the bug
Co-Authored-By: Claude Opus 4.6
---
Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +-
MediaBrowser.Model/Dlna/StreamInfo.cs | 12 +++++++++---
tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs | 6 ++----
3 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 1e984542ec..c6823fa807 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -268,7 +268,7 @@ public static class StreamingHelpers
Dictionary streamOptions = new Dictionary();
foreach (var param in queryString)
{
- if (char.IsLower(param.Key[0]))
+ if (param.Key.Length > 0 && char.IsLower(param.Key[0]))
{
// This was probably not parsed initially and should be a StreamOptions
// or the generated URL should correctly serialize it
diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 551bee89e3..7aad97ce01 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -895,7 +895,7 @@ public class StreamInfo
if (SubProtocol == MediaStreamProtocol.hls)
{
- sb.Append("/master.m3u8?");
+ sb.Append("/master.m3u8");
}
else
{
@@ -906,10 +906,10 @@ public class StreamInfo
sb.Append('.');
sb.Append(Container);
}
-
- sb.Append('?');
}
+ var queryStart = sb.Length;
+
if (!string.IsNullOrEmpty(DeviceProfileId))
{
sb.Append("&DeviceProfileId=");
@@ -1133,6 +1133,12 @@ public class StreamInfo
sb.Append(query);
}
+ // Replace the first '&' with '?' to form a valid query string.
+ if (sb.Length > queryStart)
+ {
+ sb[queryStart] = '?';
+ }
+
return sb.ToString();
}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
index 8dea468064..4b3126fe11 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs
@@ -216,8 +216,7 @@ public class StreamInfoTests
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
- // New version will return and & after the ? due to optional parameters.
- string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
+ string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
}
@@ -234,8 +233,7 @@ public class StreamInfoTests
FillAllProperties(streamInfo);
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
- // New version will return and & after the ? due to optional parameters.
- string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
+ string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
}
From 94dcaf2ea24e428f2957674ac46b864625782194 Mon Sep 17 00:00:00 2001
From: Shadowghost
Date: Wed, 18 Feb 2026 22:39:49 +0100
Subject: [PATCH 010/136] Upgrade Swashbuckle to v10
---
Directory.Packages.props | 4 +-
.../ApiApplicationBuilderExtensions.cs | 2 +-
.../ApiServiceCollectionExtensions.cs | 21 ++--
.../Filters/AdditionalModelFilter.cs | 101 ++++++++----------
.../Filters/CachingOpenApiProvider.cs | 4 +-
Jellyfin.Server/Filters/FileRequestFilter.cs | 5 +-
Jellyfin.Server/Filters/FileResponseFilter.cs | 11 +-
.../Filters/FlagsEnumSchemaFilter.cs | 23 ++--
.../Filters/IgnoreEnumSchemaFilter.cs | 17 +--
.../Filters/ParameterObsoleteFilter.cs | 12 ++-
.../RetryOnTemporarilyUnavailableFilter.cs | 10 +-
.../SecurityRequirementsOperationFilter.cs | 15 +--
.../SecuritySchemeReferenceFixupFilter.cs | 56 ++++++++++
13 files changed, 168 insertions(+), 113 deletions(-)
create mode 100644 Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a520b87e2b..cee12a48ae 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -75,8 +75,8 @@
-
-
+
+
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index 9fd853cf2e..2aadedfa61 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -3,7 +3,7 @@ using Jellyfin.Api.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
namespace Jellyfin.Server.Extensions
{
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 9df24fa0d7..c71c193e2e 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Claims;
+using System.Text.Json.Nodes;
using Emby.Server.Implementations;
using Jellyfin.Api.Auth;
using Jellyfin.Api.Auth.AnonymousLanAccessPolicy;
@@ -26,7 +26,6 @@ using Jellyfin.Server.Filters;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@@ -34,9 +33,7 @@ using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.OpenApi.Any;
-using Microsoft.OpenApi.Interfaces;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
@@ -208,7 +205,7 @@ namespace Jellyfin.Server.Extensions
{
{
"x-jellyfin-version",
- new OpenApiString(version)
+ new JsonNodeExtension(JsonValue.Create(version))
}
}
});
@@ -262,6 +259,7 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter();
c.OperationFilter();
c.DocumentFilter();
+ c.DocumentFilter();
})
.Replace(ServiceDescriptor.Transient());
}
@@ -333,10 +331,10 @@ namespace Jellyfin.Server.Extensions
options.MapType>(() =>
new OpenApiSchema
{
- Type = "object",
+ Type = JsonSchemaType.Object,
AdditionalProperties = new OpenApiSchema
{
- Type = "string"
+ Type = JsonSchemaType.String
}
});
@@ -344,18 +342,17 @@ namespace Jellyfin.Server.Extensions
options.MapType>(() =>
new OpenApiSchema
{
- Type = "object",
+ Type = JsonSchemaType.Object,
AdditionalProperties = new OpenApiSchema
{
- Type = "string",
- Nullable = true
+ Type = JsonSchemaType.String | JsonSchemaType.Null
}
});
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
options.MapType(() => new OpenApiSchema
{
- Type = "string"
+ Type = JsonSchemaType.String
});
}
}
diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
index 7407bd2eb7..efa2f4cca5 100644
--- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs
+++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs
@@ -3,18 +3,17 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
+using System.Text.Json.Nodes;
using Jellyfin.Extensions;
using Jellyfin.Server.Migrations;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
-using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.ApiClient;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
-using Microsoft.OpenApi.Any;
-using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
@@ -25,7 +24,7 @@ namespace Jellyfin.Server.Filters
public class AdditionalModelFilter : IDocumentFilter
{
// Array of options that should not be visible in the api spec.
- private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions) };
+ private static readonly Type[] _ignoredConfigurations = [typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions)];
private readonly IServerConfigurationManager _serverConfigurationManager;
///
@@ -48,8 +47,8 @@ namespace Jellyfin.Server.Filters
&& t != typeof(WebSocketMessageInfo))
.ToList();
- var inboundWebSocketSchemas = new List();
- var inboundWebSocketDiscriminators = new Dictionary();
+ var inboundWebSocketSchemas = new List();
+ var inboundWebSocketDiscriminators = new Dictionary();
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute()?.Value;
@@ -60,18 +59,16 @@ namespace Jellyfin.Server.Filters
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
inboundWebSocketSchemas.Add(schema);
- inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
+ if (schema is OpenApiSchemaReference schemaRef)
+ {
+ inboundWebSocketDiscriminators[messageType.ToString()!] = schemaRef;
+ }
}
var inboundWebSocketMessageSchema = new OpenApiSchema
{
- Type = "object",
+ Type = JsonSchemaType.Object,
Description = "Represents the list of possible inbound websocket types",
- Reference = new OpenApiReference
- {
- Id = nameof(InboundWebSocketMessage),
- Type = ReferenceType.Schema
- },
OneOf = inboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
@@ -82,8 +79,8 @@ namespace Jellyfin.Server.Filters
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
- var outboundWebSocketSchemas = new List();
- var outboundWebSocketDiscriminators = new Dictionary();
+ var outboundWebSocketSchemas = new List();
+ var outboundWebSocketDiscriminators = new Dictionary();
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute()?.Value;
@@ -94,58 +91,55 @@ namespace Jellyfin.Server.Filters
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
outboundWebSocketSchemas.Add(schema);
- outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
+ if (schema is OpenApiSchemaReference schemaRef)
+ {
+ outboundWebSocketDiscriminators.Add(messageType.ToString()!, schemaRef);
+ }
}
// Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us
var syncPlayGroupUpdateMessageSchema = new OpenApiSchema
{
- Type = "object",
+ Type = JsonSchemaType.Object,
Description = "Untyped sync play command.",
- Properties = new Dictionary
+ Properties = new Dictionary
{
{
"Data", new OpenApiSchema
{
- AllOf =
- [
- new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate
From f2ed024296336cd7b63253e8d5fb2019df8724ed Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Mon, 2 Mar 2026 20:52:57 +0100
Subject: [PATCH 024/136] Update CI dependencies (#16324)
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 66fa73d25b..00fabda93e 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@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+ uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+ uses: github/codeql-action/autobuild@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+ uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 5cb13d6947..5556bb09e9 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@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1
+ uses: danielpalme/ReportGenerator-GitHub-Action@2a7030e9775aab6c78e80cb66843051acdacee3e # v5.5.2
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
From e4500303bb59cd8671af8634881efd8548d8e86a Mon Sep 17 00:00:00 2001
From: Ori
Date: Tue, 3 Mar 2026 11:09:13 -0500
Subject: [PATCH 025/136] Translated using Weblate (Hebrew (Israel))
Translation: Jellyfin/Jellyfin Translate-URL:
https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he_IL/
---
Emby.Server.Implementations/Localization/Core/he_IL.json | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json
index 0967ef424b..67bfbccfba 100644
--- a/Emby.Server.Implementations/Localization/Core/he_IL.json
+++ b/Emby.Server.Implementations/Localization/Core/he_IL.json
@@ -1 +1,4 @@
-{}
+{
+ "Books": "ספרים",
+ "NameSeasonNumber": "עונה {0}"
+}
From 44f7d2b854d092de86def8ee73ab285c5409190e Mon Sep 17 00:00:00 2001
From: Juan
Date: Mon, 2 Mar 2026 13:15:48 -0500
Subject: [PATCH 026/136] Add missing ProducesResponseType(401) to
InitiateQuickConnect
The InitiateQuickConnect endpoint returns HTTP 401 Unauthorized when
Quick Connect is disabled, and this was already documented in the XML
response comment, but the corresponding [ProducesResponseType] attribute
was missing, causing the OpenAPI/Swagger spec to omit it.
Co-Authored-By: Claude Sonnet 4.6
---
Jellyfin.Api/Controllers/QuickConnectController.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 2a15ff767c..bdb2a4d20b 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -52,6 +52,7 @@ public class QuickConnectController : BaseJellyfinApiController
/// A with a secret and code for future use or an error message.
[HttpPost("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task> InitiateQuickConnect()
{
try
From 5807bf1d8f98e5c8ab54e240d469a0af07210854 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 3 Mar 2026 22:45:40 +0000
Subject: [PATCH 027/136] Update danielpalme/ReportGenerator-GitHub-Action
action to v5.5.3
---
.github/workflows/ci-tests.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 5556bb09e9..c5a64fd9f9 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@2a7030e9775aab6c78e80cb66843051acdacee3e # v5.5.2
+ uses: danielpalme/ReportGenerator-GitHub-Action@2a82782178b2816d9d6960a7345fdd164791b323 # v5.5.3
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
From b83378d656adef9c2e0e7df101f7da84ef762fa5 Mon Sep 17 00:00:00 2001
From: Ori
Date: Tue, 3 Mar 2026 21:03:13 -0500
Subject: [PATCH 028/136] Translated using Weblate (Hebrew (Israel))
Translation: Jellyfin/Jellyfin Translate-URL:
https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/he_IL/
---
Emby.Server.Implementations/Localization/Core/he_IL.json | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/Emby.Server.Implementations/Localization/Core/he_IL.json b/Emby.Server.Implementations/Localization/Core/he_IL.json
index 67bfbccfba..1d688f01a3 100644
--- a/Emby.Server.Implementations/Localization/Core/he_IL.json
+++ b/Emby.Server.Implementations/Localization/Core/he_IL.json
@@ -1,4 +1,8 @@
{
"Books": "ספרים",
- "NameSeasonNumber": "עונה {0}"
+ "NameSeasonNumber": "עונה {0}",
+ "Channels": "ערוצים",
+ "Movies": "סרטים",
+ "Music": "מוזיקה",
+ "Collections": "אוספים"
}
From b444d2c66a39b7421879424f9c1ea57b3780d33b Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 4 Mar 2026 16:07:01 +0000
Subject: [PATCH 029/136] Update dependency Polly to 8.6.6
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 74d2ff8717..c47f69e3cd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -57,7 +57,7 @@
-
+
From 8824f07e1b07e5dbcd0641423dc472b67c268d21 Mon Sep 17 00:00:00 2001
From: Bond_009
Date: Wed, 4 Mar 2026 20:14:21 +0100
Subject: [PATCH 030/136] 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 a7c95ab00954ff57ef76055bddc67eca15f36e35 Mon Sep 17 00:00:00 2001
From: Eugene
Date: Wed, 4 Mar 2026 15:19:55 -0500
Subject: [PATCH 031/136] Translated using Weblate (Afrikaans) Translation:
Jellyfin/Jellyfin Translate-URL:
https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/af/
---
Emby.Server.Implementations/Localization/Core/af.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 1dce589234..59fb33941b 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -135,5 +135,7 @@
"TaskExtractMediaSegments": "Media Segment Skandeer",
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
- "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
+ "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings.",
+ "CleanupUserDataTask": "Gebruikers data skoon maak taak",
+ "CleanupUserDataTaskDescription": "Maak alle gebruikers data (kykstatus, gunstelingstatus, ens.) skoon van media wat nie meer vir ten minste 90 dae teenwoordig is nie."
}
From c843c71003cf0b758089a09f345e650fb0159f55 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 5 Mar 2026 06:38:13 +0000
Subject: [PATCH 032/136] Update actions/setup-dotnet action to v5.2.0
---
.github/workflows/ci-codeql-analysis.yml | 2 +-
.github/workflows/ci-compat.yml | 4 ++--
.github/workflows/ci-openapi.yml | 4 ++--
.github/workflows/ci-tests.yml | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 00fabda93e..a93b4a5354 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
+ uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index 159492770d..bd3751d371 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -17,7 +17,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
+ uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
@@ -47,7 +47,7 @@ jobs:
fetch-depth: 0
- name: Setup .NET
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
+ uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index e267836bdd..ffb4b78149 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -22,7 +22,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
+ uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
@@ -59,7 +59,7 @@ jobs:
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
+ uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index c5a64fd9f9..7586e826b9 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
+ - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
From bc05ecd543ba8b51704012d64e637b039502f146 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Thu, 5 Mar 2026 23:57:22 +0000
Subject: [PATCH 033/136] Update github/codeql-action action to v4.32.6
---
.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 a93b4a5354..9eadf7632d 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@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
+ uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
+ uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
+ uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
From 0ebf6a6db6dbd3d3148c8075427de0516b274537 Mon Sep 17 00:00:00 2001
From: crimsonspecter <246959308+crimsonspecter@users.noreply.github.com>
Date: Fri, 6 Mar 2026 16:58:07 -0500
Subject: [PATCH 034/136] Backport pull request #16341 from
jellyfin/release-10.11.z
Fix hls segment length adjustment for remuxed content
Original-merge: 09ba04662acf256ec0a22f56de7e02d65d0805f6
Merged-by: Bond-009
Backported-by: Bond_009
---
Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index f80b36c390..acd5dd64ec 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1403,8 +1403,8 @@ public class DynamicHlsController : BaseJellyfinApiController
double fps = state.TargetFramerate ?? 0.0f;
int segmentLength = state.SegmentLength * 1000;
- // If framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
- if (Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
+ // If video is transcoded and framerate is fractional (i.e. 23.976), we need to slightly adjust segment length
+ if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001)
{
double nearestIntFramerate = Math.Ceiling(fps);
segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps));
From 382db1da0dabf26d9fec1d809c76bbcec70418d5 Mon Sep 17 00:00:00 2001
From: Shadowghost
Date: Sat, 7 Mar 2026 10:49:42 +0100
Subject: [PATCH 035/136] 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 036/136] 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 037/136] 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 d6e2fcb233c0fd8f0b1a9af03293081d0c95bb4f Mon Sep 17 00:00:00 2001
From: Denislav Denev
Date: Mon, 9 Mar 2026 20:32:08 -0400
Subject: [PATCH 038/136] Translated using Weblate (Bulgarian) Translation:
Jellyfin/Jellyfin Translate-URL:
https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bg/
---
Emby.Server.Implementations/Localization/Core/bg-BG.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 92b8e5d565..054c7357e1 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -15,7 +15,7 @@
"Favorites": "Любими",
"Folders": "Папки",
"Genres": "Жанрове",
- "HeaderAlbumArtists": "Изпълнители на албуми",
+ "HeaderAlbumArtists": "Изпълнители на албума",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
From 119b2e3d2ff275401870edb66ffcbb4569a9c678 Mon Sep 17 00:00:00 2001
From: theguymadmax
Date: Tue, 10 Mar 2026 19:04:02 -0400
Subject: [PATCH 039/136] 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 7ab1c6bb150c4e9cc41ab4c2de39e8a63704b46f Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 10 Mar 2026 23:31:43 +0000
Subject: [PATCH 040/136] Update dependency Microsoft.CodeAnalysis.Analyzers to
v5
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index c47f69e3cd..e23df8002e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -31,7 +31,7 @@
-
+
From e4eba084dd0e852e90c7677f9a8b594ecd3f7669 Mon Sep 17 00:00:00 2001
From: jaxx2104
Date: Wed, 11 Mar 2026 23:57:30 +0900
Subject: [PATCH 041/136] 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 32270576c21c31ac73d16cb14910d347fd70756f Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 11 Mar 2026 16:02:08 +0000
Subject: [PATCH 042/136] Update actions/download-artifact action to v8.0.1
---
.github/workflows/ci-compat.yml | 4 ++--
.github/workflows/ci-openapi.yml | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index bd3751d371..f9e2fbc3a6 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -85,13 +85,13 @@ jobs:
steps:
- name: Download abi-head
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: abi-head
path: abi-head
- name: Download abi-base
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: abi-base
path: abi-base
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index ffb4b78149..f4fd0829b0 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -85,13 +85,13 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-base
path: openapi-base
@@ -119,7 +119,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
@@ -180,7 +180,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openapi-head
path: openapi-head
From 8e29e5e419902d39ec98a6cdd1b9d5aee370188f Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 11 Mar 2026 19:45:48 +0000
Subject: [PATCH 043/136] Update Microsoft
---
Directory.Packages.props | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e23df8002e..54ddde2c58 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,27 +26,27 @@
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -77,7 +77,7 @@
-
+
From 6880a2ce3e16a39b2aa98b2cc2f727920295f7e3 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 11 Mar 2026 20:21:54 +0000
Subject: [PATCH 044/136] Update swashbuckle-aspnetcore monorepo to 10.1.5
---
Directory.Packages.props | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 54ddde2c58..bd720ed348 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -75,8 +75,8 @@
-
-
+
+
From 946c6b9981145d73a6cd64fc6fbcbd6d5b6961ae Mon Sep 17 00:00:00 2001
From: Bond_009
Date: Wed, 11 Mar 2026 21:20:14 +0100
Subject: [PATCH 045/136] 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 046/136] 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 047/136] 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 048/136] 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 049/136] 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 050/136] 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 051/136] 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 052/136] 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 053/136] 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 054/136] 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 055/136] 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 056/136] 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 057/136] 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 058/136] 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 059/136] 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 060/136] 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 061/136] 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 062/136] 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 063/136] 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 064/136] 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 065/136] 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 066/136] 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 300036c85913b79bf9bbf13c81ea2241f1216f78 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Mon, 23 Mar 2026 17:08:15 -0400
Subject: [PATCH 067/136] Fix FolderStorageInfo to show parent filesystem
A direct implementation using DriveInfo directly on a path does not work
as expected. The method will return a DriveInfo object with the given
path as both the Name and the RootDirectory, which is not helpful.
Instead, add parsing logic to find the best possible match out of all
filesystems on the system for the path, including handling edge cases
involving symlinked paths in the chain.
This ensures that the resulting DeviceId is a valid filesystem, allowing
it to be used in the UI to show a better description. It also includes
the new ResolvedPath which will show, if required, what the Path
resolved to after all symlinks are interpolated.
One possible issue here is that walking all drives as-is might become
slow(er) on a system with many partitions, but even on my
partition-heavy system with over a dozen ZVOLs and remote mounts, this
takes under 0.4 seconds including runup time for `dotnet run`, so I
imagine this should be fine.
---
.../StorageHelpers/StorageHelper.cs | 53 ++++++++++++++++---
1 file changed, 46 insertions(+), 7 deletions(-)
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index ce628a04d0..0b8e2830d2 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -28,22 +28,41 @@ public static class StorageHelper
}
///
- /// Gets the free space of a specific directory.
+ /// Gets the free space of the parent filesystem of a specific directory.
///
/// Path to a folder.
- /// The number of bytes available space.
+ /// Various details about the parent filesystem containing the directory.
public static FolderStorageInfo GetFreeSpaceOf(string path)
{
try
{
- var driveInfo = new DriveInfo(path);
+ // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar.
+ resolvedPath = ResolvePath(path);
+ // We iterate all filesystems reported by GetDrives() here, and attempt to find the best
+ // match that contains, as deep as possible, the given path.
+ // This is required because simply calling `DriveInfo` on a path returns that path as
+ // the Name and RootDevice, which is not at all how this should work.
+ DriveInfo[] allDrives = DriveInfo.GetDrives();
+ DriveInfo bestMatch = null;
+ foreach (DriveInfo d in allDrives)
+ {
+ if (resolvedPath.StartsWith(d.RootDirectory.FullName) &&
+ (bestMatch == null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length))
+ {
+ bestMatch = d;
+ }
+ }
+ if (bestMatch is null) {
+ throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid.");
+ }
return new FolderStorageInfo()
{
Path = path,
- FreeSpace = driveInfo.AvailableFreeSpace,
- UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace,
- StorageType = driveInfo.DriveType.ToString(),
- DeviceId = driveInfo.Name,
+ ResolvedPath = resolvedPath,
+ FreeSpace = bestMatch.AvailableFreeSpace,
+ UsedSpace = bestMatch.TotalSize - bestMatch.AvailableFreeSpace,
+ StorageType = bestMatch.DriveType.ToString(),
+ DeviceId = bestMatch.Name,
};
}
catch
@@ -59,6 +78,26 @@ public static class StorageHelper
}
}
+ ///
+ /// Walk a path and fully resolve any symlinks within it.
+ ///
+ private static string ResolvePath(string path)
+ {
+ var parts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
+ var current = Path.DirectorySeparatorChar.ToString();
+ foreach (var part in parts)
+ {
+ current = Path.Combine(current, part);
+ var resolved = new DirectoryInfo(current).ResolveLinkTarget(returnFinalTarget: true);
+ if (resolved is not null)
+ {
+ current = resolved.FullName;
+ }
+ }
+
+ return current;
+ }
+
///
/// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold.
///
From 434ebc8b110a2736c9be08360c17cf74e27803d1 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Mon, 23 Mar 2026 17:11:29 -0400
Subject: [PATCH 068/136] Ensure ResolvedPath is sent on error too
---
Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index 0b8e2830d2..a36a51330d 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -70,6 +70,7 @@ public static class StorageHelper
return new FolderStorageInfo()
{
Path = path,
+ ResolvedPath = path,
FreeSpace = -1,
UsedSpace = -1,
StorageType = null,
From 418beafebb49527974c5563907367e6b689352a3 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Mon, 23 Mar 2026 17:15:49 -0400
Subject: [PATCH 069/136] Update FolderStorageInfo record
---
MediaBrowser.Model/System/FolderStorageInfo.cs | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs
index 7b10e4ea58..66356c6c48 100644
--- a/MediaBrowser.Model/System/FolderStorageInfo.cs
+++ b/MediaBrowser.Model/System/FolderStorageInfo.cs
@@ -11,17 +11,22 @@ public record FolderStorageInfo
public required string Path { get; init; }
///
- /// Gets the free space of the underlying storage device of the .
+ /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present)
+ ///
+ public required string ResolvedPath { get; init; }
+
+ ///
+ /// Gets the free space of the underlying storage device of the .
///
public long FreeSpace { get; init; }
///
- /// Gets the used space of the underlying storage device of the .
+ /// Gets the used space of the underlying storage device of the .
///
public long UsedSpace { get; init; }
///
- /// Gets the kind of storage device of the .
+ /// Gets the kind of storage device of the .
///
public string? StorageType { get; init; }
From 8142bbd50e4c2218e99c621900430b0189c267c3 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Mon, 23 Mar 2026 17:22:35 -0400
Subject: [PATCH 070/136] Properly define variable type
---
Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index a36a51330d..b80d65ecbe 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -37,7 +37,7 @@ public static class StorageHelper
try
{
// Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar.
- resolvedPath = ResolvePath(path);
+ string resolvedPath = ResolvePath(path);
// We iterate all filesystems reported by GetDrives() here, and attempt to find the best
// match that contains, as deep as possible, the given path.
// This is required because simply calling `DriveInfo` on a path returns that path as
From 965b602c6890623130d1d7e27de52e161c6d1069 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Mon, 23 Mar 2026 23:09:56 -0400
Subject: [PATCH 071/136] Apply suggestions from code review
Co-authored-by: JPVenson
---
.../StorageHelpers/StorageHelper.cs | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index b80d65ecbe..d3f94ad0bd 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -37,24 +37,26 @@ public static class StorageHelper
try
{
// Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar.
- string resolvedPath = ResolvePath(path);
+ var resolvedPath = ResolvePath(path);
// We iterate all filesystems reported by GetDrives() here, and attempt to find the best
// match that contains, as deep as possible, the given path.
// This is required because simply calling `DriveInfo` on a path returns that path as
// the Name and RootDevice, which is not at all how this should work.
- DriveInfo[] allDrives = DriveInfo.GetDrives();
- DriveInfo bestMatch = null;
+ var allDrives = DriveInfo.GetDrives();
+ DriveInfo? bestMatch = null;
foreach (DriveInfo d in allDrives)
{
- if (resolvedPath.StartsWith(d.RootDirectory.FullName) &&
- (bestMatch == null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length))
+ if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) &&
+ (bestMatch is null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length))
{
bestMatch = d;
}
}
+
if (bestMatch is null) {
throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid.");
}
+
return new FolderStorageInfo()
{
Path = path,
From 386c4cb7236650061452026cac365229aa3b0cca Mon Sep 17 00:00:00 2001
From: scheilch
Date: Tue, 24 Mar 2026 18:02:00 +0100
Subject: [PATCH 072/136] 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 073/136] 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 c22933260b1d9b8cd97980c00a70f53bbaaf4f54 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Tue, 24 Mar 2026 22:22:52 -0400
Subject: [PATCH 074/136] Fix linting issue
---
MediaBrowser.Model/System/FolderStorageInfo.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs
index 66356c6c48..ebca39228b 100644
--- a/MediaBrowser.Model/System/FolderStorageInfo.cs
+++ b/MediaBrowser.Model/System/FolderStorageInfo.cs
@@ -11,7 +11,7 @@ public record FolderStorageInfo
public required string Path { get; init; }
///
- /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present)
+ /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present).
///
public required string ResolvedPath { get; init; }
From fec78c8448bd19f96460d853732cf24812443b70 Mon Sep 17 00:00:00 2001
From: "Joshua M. Boniface"
Date: Tue, 24 Mar 2026 22:31:17 -0400
Subject: [PATCH 075/136] Lint for the Linter Gods
---
.../StorageHelpers/StorageHelper.cs | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index d3f94ad0bd..13c7895f83 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -43,7 +43,7 @@ public static class StorageHelper
// This is required because simply calling `DriveInfo` on a path returns that path as
// the Name and RootDevice, which is not at all how this should work.
var allDrives = DriveInfo.GetDrives();
- DriveInfo? bestMatch = null;
+ DriveInfo? bestMatch = null;
foreach (DriveInfo d in allDrives)
{
if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) &&
@@ -52,11 +52,12 @@ public static class StorageHelper
bestMatch = d;
}
}
-
- if (bestMatch is null) {
+
+ if (bestMatch is null)
+ {
throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid.");
}
-
+
return new FolderStorageInfo()
{
Path = path,
From 6b443bb2ec64009f9903388aa87868d30c5c42d7 Mon Sep 17 00:00:00 2001
From: dkanada
Date: Wed, 25 Mar 2026 15:39:31 +0900
Subject: [PATCH 076/136] 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 077/136] 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 078/136] 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 079/136] 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 080/136] 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 081/136] 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 082/136] 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 083/136] 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 084/136] 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 085/136] 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 086/136] 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 087/136] 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 088/136] 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 089/136] 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 090/136] 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 091/136] 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 092/136] 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 093/136] 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 094/136] 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 095/136] 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 096/136] 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 097/136] 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 098/136] 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 099/136] 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 100/136] 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 101/136] 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 102/136] 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 103/136] 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 104/136] 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 105/136] 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 106/136] 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 107/136] 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 108/136] 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 109/136] 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 110/136] 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 111/136] 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 112/136] 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 113/136] 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 114/136] 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 115/136] 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 116/136] 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 117/136] 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 118/136] 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 119/136] 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 120/136] 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 121/136] 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 122/136] 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 123/136] 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 124/136] 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 125/136] 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 126/136] 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 127/136] 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 128/136] 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 129/136] 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 130/136] 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 131/136] 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 132/136] 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 133/136] 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 134/136] 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": "應用程式更新好咗",
From 78e0a2b5c13eba9c65779bde41362fe728d57fb2 Mon Sep 17 00:00:00 2001
From: nour ovendodger
Date: Tue, 7 Apr 2026 19:39:13 -0400
Subject: [PATCH 135/136] Translated using Weblate (Abkhazian) Translation:
Jellyfin/Jellyfin Translate-URL:
https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ab/
---
Emby.Server.Implementations/Localization/Core/ab.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json
index bc6062f429..d6d257c5ba 100644
--- a/Emby.Server.Implementations/Localization/Core/ab.json
+++ b/Emby.Server.Implementations/Localization/Core/ab.json
@@ -1,3 +1,5 @@
{
- "Albums": "аальбомқәа"
+ "Albums": "аальбомқәа",
+ "AppDeviceValues": "Апп: {0}, Априбор: {1}",
+ "Application": "Апрограмма"
}
From 68f26e5a344bfa9f8f26c02acf1ed2ccb8913712 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Wed, 8 Apr 2026 15:37:03 +0000
Subject: [PATCH 136/136] Update dependency Microsoft.NET.Test.Sdk to 18.4.0
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f15f7c7a75..bf79ff0e0e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -47,7 +47,7 @@
-
+