From 9ec657cf6e93dcf26206a2be98c2072bb63d6d23 Mon Sep 17 00:00:00 2001 From: felix920506 Date: Wed, 27 Mar 2024 13:23:10 -0400 Subject: [PATCH 001/390] Remove permission check from GHA Permission check is moved to the script itself --- .github/workflows/commands.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index d78f11166c..23636cff64 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -124,7 +124,7 @@ jobs: rename: name: Rename - if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' + if: contains(github.event.comment.body, '@jellyfin-bot rename') runs-on: ubuntu-latest steps: - name: pull in script From c46dff16ccceffcc67fbcc505790867d4c454a6e Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Sun, 11 Dec 2022 14:12:23 -0600 Subject: [PATCH 002/390] Set IsLive --- src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index 7dc30f7275..88ec0b14e6 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -170,6 +170,7 @@ namespace Jellyfin.LiveTv.Listings IsSeries = program.Episode.Episode is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, + IsLive = program.IsLive, IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), From 80365f277afa05b329422362cbef6a4aa92317cd Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 4 Mar 2024 00:28:41 -0500 Subject: [PATCH 003/390] chore(ci): Add permissions grant Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index f7366c7e04..96e72743e6 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -8,6 +8,10 @@ on: schedule: - cron: '24 2 * * 4' +permissions: + contents: read + security-events: write + jobs: analyze: name: Analyze From 47af1c4576dbfbfd438bba248b582d1f312a84cd Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 15:31:52 +0800 Subject: [PATCH 004/390] Don't allow library name with leading or trailing space Windows has a specific requirement on filenames that cannot have leading or trailing spaces and the folder creation would fail. Reject such request in the api controller. --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 55000fc91e..eb2aba8814 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -81,6 +82,12 @@ public class LibraryStructureController : BaseJellyfinApiController [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { + // Windows does not allow files or folders with names that has leading or trailing spaces + if (name.Length != name.Trim().Length) + { + return BadRequest(); + } + var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); if (paths is not null && paths.Length > 0) From 2904083053bdad516de07324b497ed268519fab1 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 15:34:04 +0800 Subject: [PATCH 005/390] Remove unused import --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index eb2aba8814..62193b1b4e 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; From b13039f377e52f5378773388ed33b6fff2d2ce50 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 16:10:25 +0800 Subject: [PATCH 006/390] Use regex attribute to validate input --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 62193b1b4e..e3cb2cd9c0 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -75,18 +75,14 @@ public class LibraryStructureController : BaseJellyfinApiController [HttpPost] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task AddVirtualFolder( - [FromQuery] string name, + [FromQuery] + [RegularExpression(@"^(?:\S(?:.*\S)?)$", ErrorMessage = "Library name cannot be empty or have leading/trailing spaces.")] + string name, [FromQuery] CollectionTypeOptions? collectionType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { - // Windows does not allow files or folders with names that has leading or trailing spaces - if (name.Length != name.Trim().Length) - { - return BadRequest(); - } - var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); if (paths is not null && paths.Length > 0) From 22ce1f25d0686b6f8a06d590289d50a7ee907845 Mon Sep 17 00:00:00 2001 From: sususu98 Date: Wed, 23 Apr 2025 18:18:38 +0800 Subject: [PATCH 007/390] refactor(StreamInfo): reorganize subtitle URL logic and conditions # Conflicts: # MediaBrowser.Model/Dlna/StreamInfo.cs --- MediaBrowser.Model/Dlna/StreamInfo.cs | 51 +++++++++++++++------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 13acd15a3f..e233797d75 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1250,34 +1250,41 @@ public class StreamInfo if (info.DeliveryMethod == SubtitleDeliveryMethod.External) { - if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal) - { - info.Url = string.Format( - CultureInfo.InvariantCulture, - "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", - baseUrl, - ItemId, - MediaSourceId, - stream.Index.ToString(CultureInfo.InvariantCulture), - startPositionTicks.ToString(CultureInfo.InvariantCulture), - subtitleProfile.Format); + // Default to using the API URL + info.Url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}", + baseUrl, + ItemId, + MediaSourceId, + stream.Index.ToString(CultureInfo.InvariantCulture), + startPositionTicks.ToString(CultureInfo.InvariantCulture), + subtitleProfile.Format); + info.IsExternalUrl = false; // Default to API URL - if (!string.IsNullOrEmpty(accessToken)) - { - info.Url += "?ApiKey=" + accessToken; - } - - info.IsExternalUrl = false; - } - else + // Check conditions for potentially using the direct path + if (stream.IsExternal // Must be external + && MediaSource?.Protocol != MediaProtocol.File // Main media must not be a local file + && string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) // Format must match (no conversion needed) + && !string.IsNullOrEmpty(stream.Path) // Path must exist + && Uri.TryCreate(stream.Path, UriKind.Absolute, out Uri? uriResult) // Path must be an absolute URI + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) // Scheme must be HTTP or HTTPS { + // All conditions met, override with the direct path info.Url = stream.Path; info.IsExternalUrl = true; } - } - return info; - } + // Append ApiKey only if we are using the API URL + if (!info.IsExternalUrl && !string.IsNullOrEmpty(accessToken)) + { + // Use "?ApiKey=" as seen in HEAD and other parts of the code + info.Url += "?ApiKey=" + accessToken; + } + } + + return info; + } /// /// Gets the target video bit depth. From fd108ff5284f7b59fe4f0e942a05051acbf14cdc Mon Sep 17 00:00:00 2001 From: sususu98 Date: Thu, 24 Apr 2025 14:17:33 +0800 Subject: [PATCH 008/390] Style: Fix indentation in StreamInfo.cs --- MediaBrowser.Model/Dlna/StreamInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index e233797d75..e465e0e31d 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1281,10 +1281,10 @@ public class StreamInfo // Use "?ApiKey=" as seen in HEAD and other parts of the code info.Url += "?ApiKey=" + accessToken; } - } + } return info; - } + } /// /// Gets the target video bit depth. From aebabb15801a2d216f84d6a49d8b434ae59e1f34 Mon Sep 17 00:00:00 2001 From: sususu98 Date: Thu, 24 Apr 2025 14:25:12 +0800 Subject: [PATCH 009/390] style: fix return statement indentation in StreamInfo.cs --- MediaBrowser.Model/Dlna/StreamInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index e465e0e31d..92404de508 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1283,7 +1283,7 @@ public class StreamInfo } } - return info; + return info; } /// From 2b6febc8da5af1018a1fa73a873bdc617dc1f084 Mon Sep 17 00:00:00 2001 From: loop Date: Mon, 25 Aug 2025 10:28:20 +0200 Subject: [PATCH 010/390] Fix: Add 'Kairon; IRSE!' to artist whitelist --- MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 997c7d2a48..e908bf6bc5 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -81,6 +81,7 @@ namespace MediaBrowser.MediaEncoding.Probing "Smith/Kotzen", "We;Na", "LSR/CITY", + "Kairon; IRSE!", }; /// From da31d0c6a61ff0d908ad3c9ddcfa42a055f7ef6f Mon Sep 17 00:00:00 2001 From: Cody Engel Date: Sat, 20 Sep 2025 14:04:00 -0600 Subject: [PATCH 011/390] support series that are numeric only updates SeriesResolver to handle series names that only contain numbers such as 1923. --- Emby.Naming/TV/SeriesResolver.cs | 21 +++++++++++++++++++ .../TV/SeriesResolverTests.cs | 2 ++ 2 files changed, 23 insertions(+) diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs index c955b8a0db..795c63509c 100644 --- a/Emby.Naming/TV/SeriesResolver.cs +++ b/Emby.Naming/TV/SeriesResolver.cs @@ -17,6 +17,13 @@ namespace Emby.Naming.TV [GeneratedRegex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))")] private static partial Regex SeriesNameRegex(); + /// + /// Regex that matches titles with year in parentheses, optionally preceded by a minus sign. + /// Captures the title (which may be numeric) before the year. + /// + [GeneratedRegex(@"^-?(?.+?)\s*\(\d{4}\)")] + private static partial Regex TitleWithYearRegex(); + /// <summary> /// Resolve information about series from path. /// </summary> @@ -27,6 +34,20 @@ namespace Emby.Naming.TV { string seriesName = Path.GetFileName(path); + // First check if the filename matches a title with year pattern (handles numeric titles) + if (!string.IsNullOrEmpty(seriesName)) + { + var titleWithYearMatch = TitleWithYearRegex().Match(seriesName); + if (titleWithYearMatch.Success) + { + seriesName = titleWithYearMatch.Groups["title"].Value.Trim(); + return new SeriesInfo(path) + { + Name = seriesName + }; + } + } + SeriesPathParserResult result = SeriesPathParser.Parse(options, path); if (result.Success) { diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs index 84758c9c36..84311b0e20 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs @@ -19,6 +19,8 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/some/path/The Show", "The Show")] [InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")] [InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")] + [InlineData("/some/path/1923 (2022)", "1923")] + [InlineData("/some/path/-1923 (2022)", "1923")] public void SeriesResolverResolveTest(string path, string name) { var res = SeriesResolver.Resolve(_namingOptions, path); From bd9a44ce7d45bf00d928ccfb13a2695686b91264 Mon Sep 17 00:00:00 2001 From: Cody Engel <cengel815@gmail.com> Date: Sat, 20 Sep 2025 18:00:44 -0600 Subject: [PATCH 012/390] =?UTF-8?q?remove=20explicit=20=E2=80=98-=E2=80=98?= =?UTF-8?q?=20support=20in=20series=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.Naming/TV/SeriesResolver.cs | 2 +- tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs index 795c63509c..6ec420a371 100644 --- a/Emby.Naming/TV/SeriesResolver.cs +++ b/Emby.Naming/TV/SeriesResolver.cs @@ -21,7 +21,7 @@ namespace Emby.Naming.TV /// Regex that matches titles with year in parentheses, optionally preceded by a minus sign. /// Captures the title (which may be numeric) before the year. /// </summary> - [GeneratedRegex(@"^-?(?<title>.+?)\s*\(\d{4}\)")] + [GeneratedRegex(@"(?<title>.+?)\s*\(\d{4}\)")] private static partial Regex TitleWithYearRegex(); /// <summary> diff --git a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs index 84311b0e20..b81b7934cd 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeriesResolverTests.cs @@ -20,7 +20,6 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/some/path/The Show s02e10 720p hdtv", "The Show")] [InlineData("/some/path/The Show s02e10 the episode 720p hdtv", "The Show")] [InlineData("/some/path/1923 (2022)", "1923")] - [InlineData("/some/path/-1923 (2022)", "1923")] public void SeriesResolverResolveTest(string path, string name) { var res = SeriesResolver.Resolve(_namingOptions, path); From 2508e8349be5b8052a68ad73316845f4668d4f43 Mon Sep 17 00:00:00 2001 From: Cody Engel <cengel815@gmail.com> Date: Tue, 23 Sep 2025 08:22:00 -0600 Subject: [PATCH 013/390] update summary docs Signed-off-by: Cody Engel <cengel815@gmail.com> --- Emby.Naming/TV/SeriesResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs index 6ec420a371..0b7309bae0 100644 --- a/Emby.Naming/TV/SeriesResolver.cs +++ b/Emby.Naming/TV/SeriesResolver.cs @@ -18,8 +18,8 @@ namespace Emby.Naming.TV private static partial Regex SeriesNameRegex(); /// <summary> - /// Regex that matches titles with year in parentheses, optionally preceded by a minus sign. - /// Captures the title (which may be numeric) before the year. + /// Regex that matches titles with year in parentheses. Captures the title (which may be + /// numeric) before the year, i.e. turns "1923 (2022)" into "1923". /// </summary> [GeneratedRegex(@"(?<title>.+?)\s*\(\d{4}\)")] private static partial Regex TitleWithYearRegex(); From 0fb6d930e1ca14d1d3af06ecee310869d2e86dfe Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Sun, 5 Oct 2025 10:59:10 +0200 Subject: [PATCH 014/390] Deprecate HasPassword property on UserDto --- .../Users/DefaultAuthenticationProvider.cs | 6 +--- .../Users/InvalidAuthProvider.cs | 6 ---- .../Users/UserManager.cs | 3 -- .../Authentication/IAuthenticationProvider.cs | 2 -- MediaBrowser.Model/Dto/UserDto.cs | 9 ++++-- .../Controllers/UserControllerTests.cs | 32 ------------------- 6 files changed, 7 insertions(+), 51 deletions(-) diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index 35c43b176d..446849b6f3 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users } // As long as jellyfin supports password-less users, we need this little block here to accommodate - if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) + if (string.IsNullOrEmpty(resolvedUser.Password) && string.IsNullOrEmpty(password)) { return Task.FromResult(new ProviderAuthenticationResult { @@ -93,10 +93,6 @@ namespace Jellyfin.Server.Implementations.Users }); } - /// <inheritdoc /> - public bool HasPassword(User user) - => !string.IsNullOrEmpty(user?.Password); - /// <inheritdoc /> public Task ChangePassword(User user, string newPassword) { diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs index caf9d5bd9a..56b8a7fc4c 100644 --- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -21,12 +21,6 @@ namespace Jellyfin.Server.Implementations.Users throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); } - /// <inheritdoc /> - public bool HasPassword(User user) - { - return true; - } - /// <inheritdoc /> public Task ChangePassword(User user, string newPassword) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index d0b41a7f6b..9f36624bce 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -306,15 +306,12 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public UserDto GetUserDto(User user, string? remoteEndPoint = null) { - var hasPassword = GetAuthenticationProvider(user).HasPassword(user); var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications; return new UserDto { Name = user.Username, Id = user.Id, ServerId = _appHost.SystemId, - HasPassword = hasPassword, - HasConfiguredPassword = hasPassword, EnableAutoLogin = user.EnableAutoLogin, LastLoginDate = user.LastLoginDate, LastActivityDate = user.LastActivityDate, diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index 976a667acd..c993ceea8e 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -14,8 +14,6 @@ namespace MediaBrowser.Controller.Authentication Task<ProviderAuthenticationResult> Authenticate(string username, string password); - bool HasPassword(User user); - Task ChangePassword(User user, string newPassword); } diff --git a/MediaBrowser.Model/Dto/UserDto.cs b/MediaBrowser.Model/Dto/UserDto.cs index 05019741e0..c6b4a4d141 100644 --- a/MediaBrowser.Model/Dto/UserDto.cs +++ b/MediaBrowser.Model/Dto/UserDto.cs @@ -1,5 +1,6 @@ #nullable disable using System; +using System.ComponentModel; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Users; @@ -54,20 +55,22 @@ namespace MediaBrowser.Model.Dto /// Gets or sets a value indicating whether this instance has password. /// </summary> /// <value><c>true</c> if this instance has password; otherwise, <c>false</c>.</value> - public bool HasPassword { get; set; } + [Obsolete("This information is no longer provided")] + public bool? HasPassword { get; set; } = true; /// <summary> /// Gets or sets a value indicating whether this instance has configured password. /// </summary> /// <value><c>true</c> if this instance has configured password; otherwise, <c>false</c>.</value> - public bool HasConfiguredPassword { get; set; } + [Obsolete("This is always true")] + public bool? HasConfiguredPassword { get; set; } = true; /// <summary> /// Gets or sets a value indicating whether this instance has configured easy password. /// </summary> /// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value> [Obsolete("Easy Password has been replaced with Quick Connect")] - public bool HasConfiguredEasyPassword { get; set; } + public bool? HasConfiguredEasyPassword { get; set; } = false; /// <summary> /// Gets or sets whether async login is enabled or not. diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 16c63ed49e..4278fd0695 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -61,7 +61,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions); Assert.NotNull(users); Assert.Single(users); - Assert.False(users![0].HasConfiguredPassword); } [Fact] @@ -92,8 +91,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions); Assert.Equal(TestUsername, user!.Name); - Assert.False(user.HasPassword); - Assert.False(user.HasConfiguredPassword); _testUserId = user.Id; @@ -149,35 +146,6 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await UpdateUserPassword(client, _testUserId, createRequest); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await client.GetStreamAsync("Users"), _jsonOptions); - var user = users!.First(x => x.Id.Equals(_testUserId)); - Assert.True(user.HasPassword); - Assert.True(user.HasConfiguredPassword); - } - - [Fact] - [Priority(2)] - public async Task UpdateUserPassword_Empty_RemoveSetPassword() - { - var client = _factory.CreateClient(); - - client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); - - var createRequest = new UpdateUserPassword() - { - CurrentPw = "4randomPa$$word", - }; - - using var response = await UpdateUserPassword(client, _testUserId, createRequest); - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await client.GetStreamAsync("Users"), _jsonOptions); - var user = users!.First(x => x.Id.Equals(_testUserId)); - Assert.False(user.HasPassword); - Assert.False(user.HasConfiguredPassword); } } } From d43db230fa1ff6f371a9ab16063152f5b632e2e9 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Sun, 19 Oct 2025 09:45:55 +0200 Subject: [PATCH 015/390] Add back UpdateUserPassword_Empty_RemoveSetPassword test --- .../Controllers/UserControllerTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 4278fd0695..04d1b3dc27 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -147,5 +147,22 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await UpdateUserPassword(client, _testUserId, createRequest); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } + + [Fact] + [Priority(2)] + public async Task UpdateUserPassword_Empty_RemoveSetPassword() + { + var client = _factory.CreateClient(); + + client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); + + var createRequest = new UpdateUserPassword() + { + CurrentPw = "4randomPa$$word", + }; + + using var response = await UpdateUserPassword(client, _testUserId, createRequest); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } } } From cd9154f1100f2133fc4f8aaa4d021c1848222e32 Mon Sep 17 00:00:00 2001 From: Kevin G <kevin@myplaceonline.com> Date: Wed, 22 Oct 2025 22:17:28 -0500 Subject: [PATCH 016/390] Add moveToTop option to IPlaylistManager.AddItemToPlaylistAsync Signed-off-by: Kevin G <kevin@myplaceonline.com> --- .../Playlists/PlaylistManager.cs | 26 ++++++++++++++----- .../Controllers/PlaylistsController.cs | 4 ++- .../Playlists/IPlaylistManager.cs | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index c9d76df0bf..6ce2883d16 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -198,17 +198,22 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(items, user, options); } - public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) + public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, bool? moveToTop, Guid userId) { var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); - return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) - { - EnableImages = true - }); + return AddToPlaylistInternal( + playlistId, + itemIds, + user, + new DtoOptions(false) + { + EnableImages = true + }, + moveToTop); } - private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options) + private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options, bool? moveToTop = null) { // Retrieve the existing playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist @@ -243,7 +248,14 @@ namespace Emby.Server.Implementations.Playlists } // Update the playlist in the repository - playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; + if (moveToTop.HasValue && moveToTop.Value) + { + playlist.LinkedChildren = [.. childrenToAdd, .. playlist.LinkedChildren]; + } + else + { + playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; + } await UpdatePlaylistInternal(playlist).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 79c71d23a4..6cf403c953 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -359,6 +359,7 @@ public class PlaylistsController : BaseJellyfinApiController /// </summary> /// <param name="playlistId">The playlist id.</param> /// <param name="ids">Item id, comma delimited.</param> + /// <param name="moveToTop">Optional. Whether to move the added item to the top.</param> /// <param name="userId">The userId.</param> /// <response code="204">Items added to playlist.</response> /// <response code="403">Access forbidden.</response> @@ -371,6 +372,7 @@ public class PlaylistsController : BaseJellyfinApiController public async Task<ActionResult> AddItemToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, + [FromQuery] bool? moveToTop, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); @@ -388,7 +390,7 @@ public class PlaylistsController : BaseJellyfinApiController return Forbid(); } - await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); + await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, moveToTop, userId.Value).ConfigureAwait(false); return NoContent(); } diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 497c4a511e..3d265f6915 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -61,9 +61,10 @@ namespace MediaBrowser.Controller.Playlists /// </summary> /// <param name="playlistId">The playlist identifier.</param> /// <param name="itemIds">The item ids.</param> + /// <param name="moveToTop">Optional. Whether to move the added item to the top.</param> /// <param name="userId">The user identifier.</param> /// <returns>Task.</returns> - Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId); + Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, bool? moveToTop, Guid userId); /// <summary> /// Removes from playlist. From 79061f463507b98f8b03db3431d23789d90de412 Mon Sep 17 00:00:00 2001 From: Kevin G <kevin@myplaceonline.com> Date: Thu, 23 Oct 2025 19:27:34 -0500 Subject: [PATCH 017/390] Change moveToTop in AddItemToPlaylistAsync to 0-based position Signed-off-by: Kevin G <kevin@myplaceonline.com> --- .../Playlists/PlaylistManager.cs | 25 +++++++++++++++---- .../Controllers/PlaylistsController.cs | 6 ++--- .../Playlists/IPlaylistManager.cs | 4 +-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 6ce2883d16..4538fc6a3f 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -198,7 +198,7 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(items, user, options); } - public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, bool? moveToTop, Guid userId) + public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId) { var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); @@ -210,10 +210,10 @@ namespace Emby.Server.Implementations.Playlists { EnableImages = true }, - moveToTop); + position); } - private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options, bool? moveToTop = null) + private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options, int? position = null) { // Retrieve the existing playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist @@ -248,9 +248,24 @@ namespace Emby.Server.Implementations.Playlists } // Update the playlist in the repository - if (moveToTop.HasValue && moveToTop.Value) + if (position.HasValue) { - playlist.LinkedChildren = [.. childrenToAdd, .. playlist.LinkedChildren]; + if (position.Value <= 0) + { + playlist.LinkedChildren = [.. childrenToAdd, .. playlist.LinkedChildren]; + } + else if (position.Value >= playlist.LinkedChildren.Length) + { + playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; + } + else + { + playlist.LinkedChildren = [ + .. playlist.LinkedChildren[0..position.Value], + .. childrenToAdd, + .. playlist.LinkedChildren[position.Value..playlist.LinkedChildren.Length] + ]; + } } else { diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 6cf403c953..1076d1eb06 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -359,7 +359,7 @@ public class PlaylistsController : BaseJellyfinApiController /// </summary> /// <param name="playlistId">The playlist id.</param> /// <param name="ids">Item id, comma delimited.</param> - /// <param name="moveToTop">Optional. Whether to move the added item to the top.</param> + /// <param name="position">Optional. 0-based index where to place the items or at the end if null.</param> /// <param name="userId">The userId.</param> /// <response code="204">Items added to playlist.</response> /// <response code="403">Access forbidden.</response> @@ -372,7 +372,7 @@ public class PlaylistsController : BaseJellyfinApiController public async Task<ActionResult> AddItemToPlaylist( [FromRoute, Required] Guid playlistId, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] Guid[] ids, - [FromQuery] bool? moveToTop, + [FromQuery] int? position, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); @@ -390,7 +390,7 @@ public class PlaylistsController : BaseJellyfinApiController return Forbid(); } - await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, moveToTop, userId.Value).ConfigureAwait(false); + await _playlistManager.AddItemToPlaylistAsync(playlistId, ids, position, userId.Value).ConfigureAwait(false); return NoContent(); } diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs index 3d265f6915..92aa923968 100644 --- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs +++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs @@ -61,10 +61,10 @@ namespace MediaBrowser.Controller.Playlists /// </summary> /// <param name="playlistId">The playlist identifier.</param> /// <param name="itemIds">The item ids.</param> - /// <param name="moveToTop">Optional. Whether to move the added item to the top.</param> + /// <param name="position">Optional. 0-based index where to place the items or at the end if null.</param> /// <param name="userId">The user identifier.</param> /// <returns>Task.</returns> - Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, bool? moveToTop, Guid userId); + Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, int? position, Guid userId); /// <summary> /// Removes from playlist. From 42ddcfa565c328c282e066346d8959a670dee366 Mon Sep 17 00:00:00 2001 From: Kevin G <kevin@myplaceonline.com> Date: Sun, 26 Oct 2025 10:29:29 -0500 Subject: [PATCH 018/390] Add milliseconds to default console output format Signed-off-by: Kevin G <kevin@myplaceonline.com> --- Jellyfin.Server/Resources/Configuration/logging.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Resources/Configuration/logging.json b/Jellyfin.Server/Resources/Configuration/logging.json index f64a85219d..ac5d9f60bc 100644 --- a/Jellyfin.Server/Resources/Configuration/logging.json +++ b/Jellyfin.Server/Resources/Configuration/logging.json @@ -11,7 +11,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}" + "outputTemplate": "[{Timestamp:HH:mm:ss.fff}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } }, { From 81f1cc78b243f84a1d6ae15cbd43b8540c450fed Mon Sep 17 00:00:00 2001 From: JPVenson <github@jpb.email> Date: Mon, 27 Oct 2025 13:01:52 +0000 Subject: [PATCH 019/390] Add version to StartupUI --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 + Jellyfin.Server/ServerSetupApp/index.mstemplate.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 72626e8532..c04f75d2e0 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -249,6 +249,7 @@ public sealed class SetupServer : IDisposable { { "isInReportingMode", _isUnhealthy }, { "retryValue", retryAfterValue }, + { "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version! }, { "logs", startupLogEntries }, { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) } }, diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html index 523f38d74a..e5db259cfa 100644 --- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html +++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html @@ -173,7 +173,7 @@ <header class="flex-row"> {{^IF isInReportingMode}} - <p>Jellyfin Server still starting. Please wait.</p> + <p>Jellyfin Server {{version}} still starting. Please wait.</p> {{#ELSE}} <p>Jellyfin Server has encountered an error and was not able to start.</p> {{/ELSE}} From 217ea488df774f9d6f920a44413acf721100efd8 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Fri, 7 Nov 2025 09:39:23 -0500 Subject: [PATCH 020/390] Fix episode showing up on recently added shows --- Jellyfin.Api/Controllers/UserLibraryController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 0e04beb14e..210bf171ae 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -13,6 +13,7 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; @@ -569,7 +570,7 @@ public class UserLibraryController : BaseJellyfinApiController var item = i.Item2[0]; var childCount = 0; - if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum)) + if (i.Item1 is not null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum || i.Item1 is Series )) { item = i.Item1; childCount = i.Item2.Count; From ac81ddd39ade7e9943a3dd25abced88070382d5b Mon Sep 17 00:00:00 2001 From: Kirill Nikiforov <me@allmazz.me> Date: Sat, 8 Nov 2025 02:54:53 +0400 Subject: [PATCH 021/390] add support for more embedded metadata tags --- .../Probing/ProbeResultNormalizer.cs | 7 ++-- .../DictionaryExtensions.cs | 33 ++++--------------- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index eb312029a1..1823496bf1 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -154,11 +154,12 @@ namespace MediaBrowser.MediaEncoding.Probing info.Name = tags.GetFirstNotNullNorWhiteSpaceValue("title", "title-eng"); info.ForcedSortName = tags.GetFirstNotNullNorWhiteSpaceValue("sort_name", "title-sort", "titlesort"); - info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc"); + info.Overview = tags.GetFirstNotNullNorWhiteSpaceValue("synopsis", "description", "desc", "comment"); - info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort"); info.ParentIndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "season_number"); - info.ShowName = tags.GetValueOrDefault("show_name"); + info.IndexNumber = FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_sort") ?? + FFProbeHelpers.GetDictionaryNumericValue(tags, "episode_id"); + info.ShowName = tags.GetValueOrDefault("show_name", "show"); info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date"); // Several different forms of retail/premiere date diff --git a/src/Jellyfin.Extensions/DictionaryExtensions.cs b/src/Jellyfin.Extensions/DictionaryExtensions.cs index 5bb828d016..814297093d 100644 --- a/src/Jellyfin.Extensions/DictionaryExtensions.cs +++ b/src/Jellyfin.Extensions/DictionaryExtensions.cs @@ -7,31 +7,6 @@ namespace Jellyfin.Extensions /// </summary> public static class DictionaryExtensions { - /// <summary> - /// Gets a string from a string dictionary, checking all keys sequentially, - /// stopping at the first key that returns a result that's neither null nor blank. - /// </summary> - /// <param name="dictionary">The dictionary.</param> - /// <param name="key1">The first checked key.</param> - /// <returns>System.String.</returns> - public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1) - { - return dictionary.GetFirstNotNullNorWhiteSpaceValue(key1, string.Empty, string.Empty); - } - - /// <summary> - /// Gets a string from a string dictionary, checking all keys sequentially, - /// stopping at the first key that returns a result that's neither null nor blank. - /// </summary> - /// <param name="dictionary">The dictionary.</param> - /// <param name="key1">The first checked key.</param> - /// <param name="key2">The second checked key.</param> - /// <returns>System.String.</returns> - public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string key2) - { - return dictionary.GetFirstNotNullNorWhiteSpaceValue(key1, key2, string.Empty); - } - /// <summary> /// Gets a string from a string dictionary, checking all keys sequentially, /// stopping at the first key that returns a result that's neither null nor blank. @@ -40,8 +15,9 @@ namespace Jellyfin.Extensions /// <param name="key1">The first checked key.</param> /// <param name="key2">The second checked key.</param> /// <param name="key3">The third checked key.</param> + /// <param name="key4">The fourth checked key.</param> /// <returns>System.String.</returns> - public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string key2, string key3) + public static string? GetFirstNotNullNorWhiteSpaceValue(this IReadOnlyDictionary<string, string> dictionary, string key1, string? key2 = null, string? key3 = null, string? key4 = null) { if (dictionary.TryGetValue(key1, out var val) && !string.IsNullOrWhiteSpace(val)) { @@ -58,6 +34,11 @@ namespace Jellyfin.Extensions return val; } + if (!string.IsNullOrEmpty(key4) && dictionary.TryGetValue(key4, out val) && !string.IsNullOrWhiteSpace(val)) + { + return val; + } + return null; } } From 5182aec13f853839cddb74ae0ebcc6b35009dc04 Mon Sep 17 00:00:00 2001 From: Karolis <karoliscd@gmail.com> Date: Mon, 17 Nov 2025 15:18:29 +0200 Subject: [PATCH 022/390] Add subtitle extraction timeout configuration option --- .../Subtitles/SubtitleEncoder.cs | 16 ++++++++++++---- .../Configuration/EncodingOptions.cs | 6 ++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 88a7bb4b41..49ac0fa033 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -13,8 +13,10 @@ using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; @@ -37,6 +39,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IMediaSourceManager _mediaSourceManager; private readonly ISubtitleParser _subtitleParser; private readonly IPathManager _pathManager; + private readonly IServerConfigurationManager _serverConfigurationManager; /// <summary> /// The _semaphoreLocks. @@ -54,7 +57,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles IHttpClientFactory httpClientFactory, IMediaSourceManager mediaSourceManager, ISubtitleParser subtitleParser, - IPathManager pathManager) + IPathManager pathManager, + IServerConfigurationManager serverConfigurationManager) { _logger = logger; _fileSystem = fileSystem; @@ -63,6 +67,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _mediaSourceManager = mediaSourceManager; _subtitleParser = subtitleParser; _pathManager = pathManager; + _serverConfigurationManager = serverConfigurationManager; } private MemoryStream ConvertSubtitles( @@ -394,7 +399,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes; + await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false); exitCode = process.ExitCode; } catch (OperationCanceledException) @@ -677,7 +683,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes; + await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false); exitCode = process.ExitCode; } catch (OperationCanceledException) @@ -828,7 +835,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes; + await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false); exitCode = process.ExitCode; } catch (OperationCanceledException) diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 2720c0bdf6..f7f386d289 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -57,6 +57,7 @@ public class EncodingOptions AllowHevcEncoding = false; AllowAv1Encoding = false; EnableSubtitleExtraction = true; + SubtitleExtractionTimeoutMinutes = 30; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"]; HardwareDecodingCodecs = ["h264", "vc1"]; } @@ -286,6 +287,11 @@ public class EncodingOptions /// </summary> public bool EnableSubtitleExtraction { get; set; } + /// <summary> + /// Gets or sets the timeout for subtitle extraction in minutes. + /// </summary> + public int SubtitleExtractionTimeoutMinutes { get; set; } + /// <summary> /// Gets or sets the codecs hardware encoding is used for. /// </summary> From 196c243a7d53d06a5fe492942442d48bba9727c4 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Tue, 18 Nov 2025 16:13:48 +0100 Subject: [PATCH 023/390] Disable legacy authorization methods by default --- .../Routines/DisableLegacyAuthorization.cs | 30 +++++++++++++++++++ .../Configuration/ServerConfiguration.cs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs diff --git a/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs b/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs new file mode 100644 index 0000000000..8bfb0c4e8e --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs @@ -0,0 +1,30 @@ +using MediaBrowser.Controller.Configuration; + +namespace Jellyfin.Server.Migrations.Routines; + +/// <summary> +/// Migration to disable legacy authorization in the system config. +/// </summary> +#pragma warning disable CS0618 // Type or member is obsolete +[JellyfinMigration("2025-11-18T16:00:00", nameof(DisableLegacyAuthorization), "F020F843-E079-4061-99E0-F43D145F2557")] +public class DisableLegacyAuthorization : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete +{ + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="DisableLegacyAuthorization"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public DisableLegacyAuthorization(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// <inheritdoc /> + public void Perform() + { + _serverConfigurationManager.Configuration.EnableLegacyAuthorization = false; + _serverConfigurationManager.SaveConfiguration(); + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index a58c01c960..ac5c12304e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -287,5 +287,5 @@ public class ServerConfiguration : BaseApplicationConfiguration /// <summary> /// Gets or sets a value indicating whether old authorization methods are allowed. /// </summary> - public bool EnableLegacyAuthorization { get; set; } = true; + public bool EnableLegacyAuthorization { get; set; } } From 67c67df5077af5c90242fc5fc36373b2ae6f2c91 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Thu, 20 Nov 2025 22:11:55 +0100 Subject: [PATCH 024/390] Use async migration --- .../Routines/DisableLegacyAuthorization.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs b/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs index 8bfb0c4e8e..6edfcbcfd5 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableLegacyAuthorization.cs @@ -1,3 +1,5 @@ +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; namespace Jellyfin.Server.Migrations.Routines; @@ -5,10 +7,8 @@ namespace Jellyfin.Server.Migrations.Routines; /// <summary> /// Migration to disable legacy authorization in the system config. /// </summary> -#pragma warning disable CS0618 // Type or member is obsolete -[JellyfinMigration("2025-11-18T16:00:00", nameof(DisableLegacyAuthorization), "F020F843-E079-4061-99E0-F43D145F2557")] -public class DisableLegacyAuthorization : IMigrationRoutine -#pragma warning restore CS0618 // Type or member is obsolete +[JellyfinMigration("2025-11-18T16:00:00", nameof(DisableLegacyAuthorization))] +public class DisableLegacyAuthorization : IAsyncMigrationRoutine { private readonly IServerConfigurationManager _serverConfigurationManager; @@ -22,9 +22,11 @@ public class DisableLegacyAuthorization : IMigrationRoutine } /// <inheritdoc /> - public void Perform() + public Task PerformAsync(CancellationToken cancellationToken) { _serverConfigurationManager.Configuration.EnableLegacyAuthorization = false; _serverConfigurationManager.SaveConfiguration(); + + return Task.CompletedTask; } } From 6963d9588080f23d351a95051696b6c692afc2c5 Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Sat, 22 Nov 2025 16:25:59 +0900 Subject: [PATCH 025/390] extract local metadata from OPF and EPUB files --- .../OpenPackagingFormat/EpubImageProvider.cs | 118 +++++++ .../Books/OpenPackagingFormat/EpubProvider.cs | 100 ++++++ .../Books/OpenPackagingFormat/EpubUtils.cs | 36 ++ .../Books/OpenPackagingFormat/OpfProvider.cs | 94 +++++ .../Books/OpenPackagingFormat/OpfReader.cs | 325 ++++++++++++++++++ 5 files changed, 673 insertions(+) create mode 100644 MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs create mode 100644 MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs create mode 100644 MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs create mode 100644 MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs create mode 100644 MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs new file mode 100644 index 0000000000..33d2823de6 --- /dev/null +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Books.OpenPackagingFormat +{ + /// <summary> + /// Provides the primary image for EPUB items that have embedded covers. + /// </summary> + public class EpubImageProvider : IDynamicImageProvider + { + private readonly ILogger<EpubImageProvider> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="EpubImageProvider"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{EpubImageProvider}"/> interface.</param> + public EpubImageProvider(ILogger<EpubImageProvider> logger) + { + _logger = logger; + } + + /// <inheritdoc /> + public string Name => "EPUB Metadata"; + + /// <inheritdoc /> + public bool Supports(BaseItem item) + { + return item is Book; + } + + /// <inheritdoc /> + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) + { + yield return ImageType.Primary; + } + + /// <inheritdoc /> + public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken) + { + if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase)) + { + return GetFromZip(item); + } + + return Task.FromResult(new DynamicImageResponse { HasImage = false }); + } + + private async Task<DynamicImageResponse> LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory) + { + var utilities = new OpfReader<EpubImageProvider>(opf, _logger); + var coverReference = utilities.ReadCoverPath(opfRootDirectory); + if (coverReference == null) + { + return new DynamicImageResponse { HasImage = false }; + } + + var cover = coverReference.Value; + var coverFile = epub.GetEntry(cover.Path); + + if (coverFile == null) + { + return new DynamicImageResponse { HasImage = false }; + } + + var memoryStream = new MemoryStream(); + using (var coverStream = coverFile.Open()) + { + await coverStream.CopyToAsync(memoryStream).ConfigureAwait(false); + } + + memoryStream.Position = 0; + + var response = new DynamicImageResponse { HasImage = true, Stream = memoryStream }; + response.SetFormatFromMimeType(cover.MimeType); + + return response; + } + + private async Task<DynamicImageResponse> GetFromZip(BaseItem item) + { + using var epub = ZipFile.OpenRead(item.Path); + + var opfFilePath = EpubUtils.ReadContentFilePath(epub); + if (opfFilePath == null) + { + return new DynamicImageResponse { HasImage = false }; + } + + var opfRootDirectory = Path.GetDirectoryName(opfFilePath); + if (opfRootDirectory == null) + { + return new DynamicImageResponse { HasImage = false }; + } + + var opfFile = epub.GetEntry(opfFilePath); + if (opfFile == null) + { + return new DynamicImageResponse { HasImage = false }; + } + + using var opfStream = opfFile.Open(); + + var opfDocument = new XmlDocument(); + opfDocument.Load(opfStream); + + return await LoadCover(epub, opfDocument, opfRootDirectory).ConfigureAwait(false); + } + } +} diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs new file mode 100644 index 0000000000..bc77e5928d --- /dev/null +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Books.OpenPackagingFormat +{ + /// <summary> + /// Provides book metadata from OPF content in an EPUB item. + /// </summary> + public class EpubProvider : ILocalMetadataProvider<Book> + { + private readonly IFileSystem _fileSystem; + private readonly ILogger<EpubProvider> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="EpubProvider"/> class. + /// </summary> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{EpubProvider}"/> interface.</param> + public EpubProvider(IFileSystem fileSystem, ILogger<EpubProvider> logger) + { + _fileSystem = fileSystem; + _logger = logger; + } + + /// <inheritdoc /> + public string Name => "EPUB Metadata"; + + /// <inheritdoc /> + public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken) + { + var path = GetEpubFile(info.Path)?.FullName; + + if (path is null) + { + return Task.FromResult(new MetadataResult<Book> { HasMetadata = false }); + } + + var result = ReadEpubAsZip(path, cancellationToken); + + if (result is null) + { + return Task.FromResult(new MetadataResult<Book> { HasMetadata = false }); + } + else + { + return Task.FromResult(result); + } + } + + private FileSystemMetadata? GetEpubFile(string path) + { + var fileInfo = _fileSystem.GetFileSystemInfo(path); + + if (fileInfo.IsDirectory) + { + return null; + } + + if (!string.Equals(Path.GetExtension(fileInfo.FullName), ".epub", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return fileInfo; + } + + private MetadataResult<Book>? ReadEpubAsZip(string path, CancellationToken cancellationToken) + { + using var epub = ZipFile.OpenRead(path); + + var opfFilePath = EpubUtils.ReadContentFilePath(epub); + if (opfFilePath == null) + { + return null; + } + + var opf = epub.GetEntry(opfFilePath); + if (opf == null) + { + return null; + } + + using var opfStream = opf.Open(); + + var opfDocument = new XmlDocument(); + opfDocument.Load(opfStream); + + var utilities = new OpfReader<EpubProvider>(opfDocument, _logger); + return utilities.ReadOpfData(cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs new file mode 100644 index 0000000000..9c1830733d --- /dev/null +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs @@ -0,0 +1,36 @@ +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Xml.Linq; + +namespace MediaBrowser.Providers.Books.OpenPackagingFormat +{ + /// <summary> + /// Utilities for EPUB files. + /// </summary> + public static class EpubUtils + { + private const string ContainerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container"; + + /// <summary> + /// Attempt to read content from ZIP archive. + /// </summary> + /// <param name="epub">The ZIP archive.</param> + /// <returns>The content file path.</returns> + public static string? ReadContentFilePath(ZipArchive epub) + { + var container = epub.GetEntry(Path.Combine("META-INF", "container.xml")); + if (container == null) + { + return null; + } + + using var containerStream = container.Open(); + + var containerDocument = XDocument.Load(containerStream); + var element = containerDocument.Descendants(ContainerNamespace + "rootfile").FirstOrDefault(); + + return element?.Attribute("full-path")?.Value; + } + } +} diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs new file mode 100644 index 0000000000..6e678802c1 --- /dev/null +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs @@ -0,0 +1,94 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Books.OpenPackagingFormat +{ + /// <summary> + /// Provides metadata for book items that have an OPF file in the same directory. Supports the standard + /// content.opf filename, bespoke metadata.opf name from Calibre libraries, and OPF files that have the + /// same name as their respective books for directories with several books. + /// </summary> + public class OpfProvider : ILocalMetadataProvider<Book>, IHasItemChangeMonitor + { + private const string StandardOpfFile = "content.opf"; + private const string CalibreOpfFile = "metadata.opf"; + + private readonly IFileSystem _fileSystem; + + private readonly ILogger<OpfProvider> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="OpfProvider"/> class. + /// </summary> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{OpfProvider}"/> interface.</param> + public OpfProvider(IFileSystem fileSystem, ILogger<OpfProvider> logger) + { + _fileSystem = fileSystem; + _logger = logger; + } + + /// <inheritdoc /> + public string Name => "Open Packaging Format"; + + /// <inheritdoc /> + public bool HasChanged(BaseItem item, IDirectoryService directoryService) + { + var file = GetXmlFile(item.Path); + + return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; + } + + /// <inheritdoc /> + public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken) + { + var path = GetXmlFile(info.Path).FullName; + + try + { + return Task.FromResult(ReadOpfData(path, cancellationToken)); + } + catch (FileNotFoundException) + { + return Task.FromResult(new MetadataResult<Book> { HasMetadata = false }); + } + } + + private FileSystemMetadata GetXmlFile(string path) + { + var fileInfo = _fileSystem.GetFileSystemInfo(path); + var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path)!); + + // check for OPF with matching name first since it's the most specific filename + var specificFile = Path.Combine(directoryInfo.FullName, Path.GetFileNameWithoutExtension(path) + ".opf"); + var file = _fileSystem.GetFileInfo(specificFile); + + if (file.Exists) + { + return file; + } + + file = _fileSystem.GetFileInfo(Path.Combine(directoryInfo.FullName, StandardOpfFile)); + + // check metadata.opf last since it's really only used by Calibre + return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryInfo.FullName, CalibreOpfFile)); + } + + private MetadataResult<Book> ReadOpfData(string file, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var doc = new XmlDocument(); + doc.Load(file); + + var utilities = new OpfReader<OpfProvider>(doc, _logger); + return utilities.ReadOpfData(cancellationToken); + } + } +} diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs new file mode 100644 index 0000000000..1a88e97089 --- /dev/null +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs @@ -0,0 +1,325 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Xml; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Net; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Books.OpenPackagingFormat +{ + /// <summary> + /// Methods used to pull metadata and other information from Open Packaging Format in XML objects. + /// </summary> + /// <typeparam name="TCategoryName">The type of category.</typeparam> + public class OpfReader<TCategoryName> + { + private const string DcNamespace = @"http://purl.org/dc/elements/1.1/"; + private const string OpfNamespace = @"http://www.idpf.org/2007/opf"; + + private readonly XmlNamespaceManager _namespaceManager; + private readonly XmlDocument _document; + + private readonly ILogger<TCategoryName> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="OpfReader{TCategoryName}"/> class. + /// </summary> + /// <param name="document">The XML document to parse.</param> + /// <param name="logger">Instance of the <see cref="ILogger{TCategoryName}"/> interface.</param> + public OpfReader(XmlDocument document, ILogger<TCategoryName> logger) + { + _document = document; + _logger = logger; + _namespaceManager = new XmlNamespaceManager(_document.NameTable); + + _namespaceManager.AddNamespace("dc", DcNamespace); + _namespaceManager.AddNamespace("opf", OpfNamespace); + } + + /// <summary> + /// Checks for the existence of a cover image. + /// </summary> + /// <param name="opfRootDirectory">The root directory in which the OPF file is located.</param> + /// <returns>Returns the found cover and its type or null.</returns> + public (string MimeType, string Path)? ReadCoverPath(string opfRootDirectory) + { + var coverImage = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@properties='cover-image']"); + if (coverImage is not null) + { + return coverImage; + } + + var coverId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='cover' and @media-type='image/*']"); + if (coverId is not null) + { + return coverId; + } + + var coverImageId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='*cover-image']"); + if (coverImageId is not null) + { + return coverImageId; + } + + var metaCoverImage = _document.SelectSingleNode("//opf:meta[@name='cover']", _namespaceManager); + var content = metaCoverImage?.Attributes?["content"]?.Value; + if (string.IsNullOrEmpty(content) || metaCoverImage is null) + { + return null; + } + + var coverPath = Path.Combine("Images", content); + var coverFileManifest = _document.SelectSingleNode($"//opf:item[@href='{coverPath}']", _namespaceManager); + var mediaType = coverFileManifest?.Attributes?["media-type"]?.Value; + if (coverFileManifest?.Attributes is not null && !string.IsNullOrEmpty(mediaType) && IsValidImage(mediaType)) + { + return (mediaType, Path.Combine(opfRootDirectory, coverPath)); + } + + var coverFileIdManifest = _document.SelectSingleNode($"//opf:item[@id='{content}']", _namespaceManager); + if (coverFileIdManifest is not null) + { + return ReadManifestItem(coverFileIdManifest, opfRootDirectory); + } + + return null; + } + + /// <summary> + /// Read all supported OPF data from the file. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>The metadata result to update.</returns> + public MetadataResult<Book> ReadOpfData(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var book = CreateBookFromOpf(); + var result = new MetadataResult<Book> { Item = book, HasMetadata = true }; + + FindAuthors(result); + ReadStringInto("//dc:language", language => result.ResultLanguage = language); + + return result; + } + + private Book CreateBookFromOpf() + { + var book = new Book + { + Name = FindMainTitle(), + ForcedSortName = FindSortTitle(), + }; + + ReadStringInto("//dc:description", summary => book.Overview = summary); + ReadStringInto("//dc:publisher", publisher => book.AddStudio(publisher)); + ReadStringInto("//dc:identifier[@opf:scheme='AMAZON']", amazon => book.SetProviderId("Amazon", amazon)); + ReadStringInto("//dc:identifier[@opf:scheme='GOOGLE']", google => book.SetProviderId("GoogleBooks", google)); + ReadStringInto("//dc:identifier[@opf:scheme='ISBN']", isbn => book.SetProviderId("ISBN", isbn)); + + ReadStringInto("//dc:date", date => + { + if (DateTime.TryParse(date, out var dateValue)) + { + book.PremiereDate = dateValue.Date; + book.ProductionYear = dateValue.Date.Year; + } + }); + + var genreNodes = _document.SelectNodes("//dc:subject", _namespaceManager); + + if (genreNodes?.Count > 0) + { + foreach (var node in genreNodes.Cast<XmlNode>().Where(node => !book.Genres.Contains(node.InnerText))) + { + book.AddGenre(node.InnerText); + } + } + + ReadInt32AttributeInto("//opf:meta[@name='calibre:series_index']", index => book.IndexNumber = index); + ReadInt32AttributeInto("//opf:meta[@name='calibre:rating']", rating => book.CommunityRating = rating); + + var seriesNameNode = _document.SelectSingleNode("//opf:meta[@name='calibre:series']", _namespaceManager); + + if (!string.IsNullOrEmpty(seriesNameNode?.Attributes?["content"]?.Value)) + { + try + { + book.SeriesName = seriesNameNode.Attributes["content"]?.Value; + } + catch (Exception) + { + _logger.LogError("error parsing Calibre series name"); + } + } + + return book; + } + + private string FindMainTitle() + { + var title = string.Empty; + var titleTypes = _document.SelectNodes("//opf:meta[@property='title-type']", _namespaceManager); + + if (titleTypes is not null && titleTypes.Count > 0) + { + foreach (XmlElement titleNode in titleTypes) + { + string refines = titleNode.GetAttribute("refines").TrimStart('#'); + string titleType = titleNode.InnerText; + + var titleElement = _document.SelectSingleNode($"//dc:title[@id='{refines}']", _namespaceManager); + if (titleElement is not null && string.Equals(titleType, "main", StringComparison.OrdinalIgnoreCase)) + { + title = titleElement.InnerText; + } + } + } + + // fallback in case there is no main title definition + if (string.IsNullOrEmpty(title)) + { + ReadStringInto("//dc:title", titleString => title = titleString); + } + + return title; + } + + private string? FindSortTitle() + { + var titleTypes = _document.SelectNodes("//opf:meta[@property='file-as']", _namespaceManager); + + if (titleTypes is not null && titleTypes.Count > 0) + { + foreach (XmlElement titleNode in titleTypes) + { + string refines = titleNode.GetAttribute("refines").TrimStart('#'); + string sortTitle = titleNode.InnerText; + + var titleElement = _document.SelectSingleNode($"//dc:title[@id='{refines}']", _namespaceManager); + if (titleElement is not null) + { + return sortTitle; + } + } + } + + // search for OPF 2.0 style title_sort node + var resultElement = _document.SelectSingleNode("//opf:meta[@name='calibre:title_sort']", _namespaceManager); + var titleSort = resultElement?.Attributes?["content"]?.Value; + + return titleSort; + } + + private void FindAuthors(MetadataResult<Book> book) + { + var resultElement = _document.SelectNodes("//dc:creator", _namespaceManager); + + if (resultElement != null && resultElement.Count > 0) + { + foreach (XmlElement creator in resultElement) + { + var creatorName = creator.InnerText; + var role = creator.GetAttribute("opf:role"); + var person = new PersonInfo { Name = creatorName, Type = GetRole(role) }; + + book.AddPerson(person); + } + } + } + + private PersonKind GetRole(string? role) + { + switch (role) + { + case "arr": + return PersonKind.Arranger; + case "art": + return PersonKind.Artist; + case "aut": + case "aqt": + case "aft": + case "aui": + default: + return PersonKind.Author; + case "edt": + return PersonKind.Editor; + case "ill": + return PersonKind.Illustrator; + case "lyr": + return PersonKind.Lyricist; + case "mus": + return PersonKind.AlbumArtist; + case "oth": + return PersonKind.Unknown; + case "trl": + return PersonKind.Translator; + } + } + + private void ReadStringInto(string xmlPath, Action<string> commitResult) + { + var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager); + if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.InnerText)) + { + commitResult(resultElement.InnerText); + } + } + + private void ReadInt32AttributeInto(string xmlPath, Action<int> commitResult) + { + var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager); + var resultValue = resultElement?.Attributes?["content"]?.Value; + + if (!string.IsNullOrEmpty(resultValue)) + { + try + { + commitResult(Convert.ToInt32(Convert.ToDouble(resultValue, CultureInfo.InvariantCulture))); + } + catch (Exception e) + { + _logger.LogError(e, "error converting to Int32"); + } + } + } + + private (string MimeType, string Path)? ReadEpubCoverInto(string opfRootDirectory, string xmlPath) + { + var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager); + + if (resultElement is not null) + { + return ReadManifestItem(resultElement, opfRootDirectory); + } + + return null; + } + + private (string MimeType, string Path)? ReadManifestItem(XmlNode manifestNode, string opfRootDirectory) + { + var href = manifestNode.Attributes?["href"]?.Value; + var mediaType = manifestNode.Attributes?["media-type"]?.Value; + + if (string.IsNullOrEmpty(href) || string.IsNullOrEmpty(mediaType) || !IsValidImage(mediaType)) + { + return null; + } + + var coverPath = Path.Combine(opfRootDirectory, href); + + return (MimeType: mediaType, Path: coverPath); + } + + private static bool IsValidImage(string? mimeType) + { + return !string.IsNullOrEmpty(mimeType) && !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType)); + } + } +} From 37bbdf3fe71e522c48c3d3ad08779e92fbf5c8b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 15:15:12 +0000 Subject: [PATCH 026/390] Update dependency z440.atl.core to 7.9.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ecbd22e3d1..c600c433ee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,7 +88,7 @@ <PackageVersion Include="System.Text.Json" Version="9.0.11" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.8.0" /> + <PackageVersion Include="z440.atl.core" Version="7.9.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> From c08e81c52b7787deab799cb7ac96cfdedc2818b7 Mon Sep 17 00:00:00 2001 From: rimasx <riks_12@hot.ee> Date: Sun, 23 Nov 2025 00:57:01 -0500 Subject: [PATCH 027/390] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index a3f9dc2f8f..2e692009bf 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -137,5 +137,5 @@ "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meediasegmendid MediaSegment'i lubavatest pluginatest.", "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "CleanupUserDataTask": "Puhasta kasutajaandmed", - "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mis pole enam vähemalt 90 päeva saadaval olnud." + "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud." } From 820e208bdcf08e757abd064eb6cd4b9a844c5c60 Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:37:56 +0900 Subject: [PATCH 028/390] fix runtime exception from incorrect argument to XDocument method --- .../Books/OpenPackagingFormat/EpubUtils.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs index 9c1830733d..e5d2987312 100644 --- a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs @@ -10,8 +10,6 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat /// </summary> public static class EpubUtils { - private const string ContainerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container"; - /// <summary> /// Attempt to read content from ZIP archive. /// </summary> @@ -27,8 +25,9 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat using var containerStream = container.Open(); + XNamespace containerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container"; var containerDocument = XDocument.Load(containerStream); - var element = containerDocument.Descendants(ContainerNamespace + "rootfile").FirstOrDefault(); + var element = containerDocument.Descendants(containerNamespace + "rootfile").FirstOrDefault(); return element?.Attribute("full-path")?.Value; } From d95bab41a1d6f030ed5ef174109573104a561b9e Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:40:52 +0900 Subject: [PATCH 029/390] improve genre support from OPF metadata --- .../Books/OpenPackagingFormat/OpfReader.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs index 1a88e97089..5d202c59e1 100644 --- a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs @@ -136,9 +136,13 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat if (genreNodes?.Count > 0) { - foreach (var node in genreNodes.Cast<XmlNode>().Where(node => !book.Genres.Contains(node.InnerText))) + foreach (var node in genreNodes.Cast<XmlNode>().Where(node => !string.IsNullOrEmpty(node.InnerText) && !book.Genres.Contains(node.InnerText))) { - book.AddGenre(node.InnerText); + // specification has no rules about content and some books combine every genre into a single element + foreach (var item in node.InnerText.Split(["/", "&", ",", ";", " - "], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + book.AddGenre(item); + } } } From cf1ef223677ee77c53e4f39f15a9fb6d824a634b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Nov 2025 14:03:55 +0000 Subject: [PATCH 030/390] Update dependency Polly to 8.6.5 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c600c433ee..210cbd35d6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -62,7 +62,7 @@ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" /> - <PackageVersion Include="Polly" Version="8.6.4" /> + <PackageVersion Include="Polly" Version="8.6.5" /> <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> From 8904551a594e0c03570989664562edf2b747a8b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:12:00 +0000 Subject: [PATCH 031/390] Update github/codeql-action action to v4.31.5 --- .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 98a0edfb16..dba18f0887 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 From c7703242e5bfbacbf7ea12a8f6cee1f75fb82fc6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 06:39:50 +0000 Subject: [PATCH 032/390] Update CI dependencies --- .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 +- .github/workflows/commands.yml | 2 +- .github/workflows/issue-template-check.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index dba18f0887..2918bd6a7c 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout repository uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index d483e048f7..298c21d7fc 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@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index a957357f3d..64742220a3 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -21,7 +21,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json @@ -55,7 +55,7 @@ jobs: 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@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index d2d649f165..b2ac7a56e6 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: ${{ env.SDK_VERSION }} diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 729bcf69bb..4ec92d42ee 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -44,7 +44,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.14' cache: 'pip' diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 1af139054a..50453d1121 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -14,7 +14,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.14' cache: 'pip' From 80e1e4294761a7a85bb405d3d1184121114b506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn?= <mrosas1690@gmail.com> Date: Fri, 28 Nov 2025 15:01:20 -0500 Subject: [PATCH 033/390] Added translation using Weblate (Occitan) --- Emby.Server.Implementations/Localization/Core/oc.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/oc.json diff --git a/Emby.Server.Implementations/Localization/Core/oc.json b/Emby.Server.Implementations/Localization/Core/oc.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/oc.json @@ -0,0 +1 @@ +{} From 6ed0ccd37c60e8dcab312b3c2c0cfb87f8bb0e13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Nov 2025 07:15:08 -0700 Subject: [PATCH 034/390] Update appleboy/ssh-action action to v1.2.4 (#15660) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 64742220a3..e1ee6ebe59 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -172,7 +172,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (unstable) into place - uses: appleboy/ssh-action@91f3272fc5907f4699dcf59761eb622a07342f5a # v1.2.3 + uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -234,7 +234,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (stable) into place - uses: appleboy/ssh-action@91f3272fc5907f4699dcf59761eb622a07342f5a # v1.2.3 + uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" From 6c507b77aeccbf83f39a9948bac98d3fed45b130 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <nielsvanvelzen@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:22:54 +0100 Subject: [PATCH 035/390] Remove DtoExtensions.AddClientFields (#15638) --- Jellyfin.Api/Controllers/ArtistsController.cs | 4 +- .../Controllers/CollectionController.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 4 +- .../Controllers/InstantMixController.cs | 7 --- Jellyfin.Api/Controllers/ItemsController.cs | 2 - Jellyfin.Api/Controllers/LibraryController.cs | 11 ++-- Jellyfin.Api/Controllers/LiveTvController.cs | 11 +--- Jellyfin.Api/Controllers/MoviesController.cs | 3 +- .../Controllers/MusicGenresController.cs | 3 +- Jellyfin.Api/Controllers/PersonsController.cs | 4 +- .../Controllers/PlaylistsController.cs | 1 - Jellyfin.Api/Controllers/StudiosController.cs | 3 +- .../Controllers/SuggestionsController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 4 -- .../Controllers/UserLibraryController.cs | 11 ++-- .../Controllers/UserViewsController.cs | 2 +- Jellyfin.Api/Controllers/VideosController.cs | 1 - Jellyfin.Api/Controllers/YearsController.cs | 4 +- Jellyfin.Api/Extensions/DtoExtensions.cs | 53 ------------------- 19 files changed, 22 insertions(+), 110 deletions(-) diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 7ba75dc243..642790f942 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -122,7 +122,6 @@ public class ArtistsController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; @@ -326,7 +325,6 @@ public class ArtistsController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = null; @@ -467,7 +465,7 @@ public class ArtistsController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var item = _libraryManager.GetArtist(name, dtoOptions); diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index c37f376335..227487b390 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -65,7 +65,7 @@ public class CollectionController : BaseJellyfinApiController UserIds = new[] { userId } }).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var dto = _dtoService.GetBaseItemDto(item, dtoOptions); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index dd60d01e0c..456e643fd7 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -94,7 +94,6 @@ public class GenresController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); User? user = userId.IsNullOrEmpty() @@ -159,8 +158,7 @@ public class GenresController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var dtoOptions = new DtoOptions() - .AddClientFields(User); + var dtoOptions = new DtoOptions(); Genre? item; if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase)) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index c4b9767565..301954561d 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -90,7 +90,6 @@ public class InstantMixController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -134,7 +133,6 @@ public class InstantMixController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -178,7 +176,6 @@ public class InstantMixController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -214,7 +211,6 @@ public class InstantMixController : BaseJellyfinApiController ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -258,7 +254,6 @@ public class InstantMixController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -302,7 +297,6 @@ public class InstantMixController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); @@ -385,7 +379,6 @@ public class InstantMixController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); return GetResult(items, user, limit, dtoOptions); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index a491283363..9674ecd092 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -268,7 +268,6 @@ public class ItemsController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 @@ -849,7 +848,6 @@ public class ItemsController : BaseJellyfinApiController var parentIdGuid = parentId ?? Guid.Empty; var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var ancestorIds = Array.Empty<Guid>(); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4c9cc2b1e8..4e898c00e8 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -187,7 +187,7 @@ public class LibraryController : BaseJellyfinApiController item = parent; } - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var items = themeItems .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); @@ -260,7 +260,7 @@ public class LibraryController : BaseJellyfinApiController item = parent; } - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var items = themeItems .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); @@ -496,7 +496,7 @@ public class LibraryController : BaseJellyfinApiController var baseItemDtos = new List<BaseItemDto>(); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); BaseItem? parent = item.GetParent(); while (parent is not null) @@ -556,7 +556,7 @@ public class LibraryController : BaseJellyfinApiController items = items.Where(i => i.IsHidden == val).ToList(); } - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var resultArray = _dtoService.GetBaseItemDtos(items, dtoOptions); return new QueryResult<BaseItemDto>(resultArray); } @@ -747,8 +747,7 @@ public class LibraryController : BaseJellyfinApiController return new QueryResult<BaseItemDto>(); } - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User); + var dtoOptions = new DtoOptions { Fields = fields }; var program = item as IHasProgramAttributes; bool? isMovie = item is Movie || (program is not null && program.IsMovie) || item is Trailer; diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 10f1789ad8..94f62a0713 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -170,7 +170,6 @@ public class LiveTvController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var channelResult = _liveTvManager.GetInternalChannels( @@ -242,8 +241,7 @@ public class LiveTvController : BaseJellyfinApiController return NotFound(); } - var dtoOptions = new DtoOptions() - .AddClientFields(User); + var dtoOptions = new DtoOptions(); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -297,7 +295,6 @@ public class LiveTvController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return await _liveTvManager.GetRecordingsAsync( @@ -444,8 +441,7 @@ public class LiveTvController : BaseJellyfinApiController return NotFound(); } - var dtoOptions = new DtoOptions() - .AddClientFields(User); + var dtoOptions = new DtoOptions(); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -635,7 +631,6 @@ public class LiveTvController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } @@ -690,7 +685,6 @@ public class LiveTvController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] } - .AddClientFields(User) .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } @@ -760,7 +754,6 @@ public class LiveTvController : BaseJellyfinApiController }; var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); return await _liveTvManager.GetRecommendedProgramsAsync(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 363acf815a..ace9a06395 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -74,8 +74,7 @@ public class MoviesController : BaseJellyfinApiController var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User); + var dtoOptions = new DtoOptions { Fields = fields }; var categories = new List<RecommendationDto>(); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 1e45e53ca1..a6427df67a 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -94,7 +94,6 @@ public class MusicGenresController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); User? user = userId.IsNullOrEmpty() @@ -148,7 +147,7 @@ public class MusicGenresController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); MusicGenre? item; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 4d12dc18fc..438d054a4c 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -81,7 +81,6 @@ public class PersonsController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = userId.IsNullOrEmpty() @@ -121,8 +120,7 @@ public class PersonsController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var dtoOptions = new DtoOptions() - .AddClientFields(User); + var dtoOptions = new DtoOptions(); var item = _libraryManager.GetPerson(name); if (item is null) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 79c71d23a4..59e6fd779d 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -548,7 +548,6 @@ public class PlaylistsController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 52cb87e72c..ad08dc5f9b 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -89,7 +89,6 @@ public class StudiosController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = userId.IsNullOrEmpty() @@ -142,7 +141,7 @@ public class StudiosController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var item = _libraryManager.GetStudio(name); if (!userId.IsNullOrEmpty()) diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 52982c362d..e9e404076f 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -77,7 +77,7 @@ public class SuggestionsController : BaseJellyfinApiController user = _userManager.GetUserById(requestUserId); } - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { (ItemSortBy.Random, SortOrder.Descending) }, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 0f08854d24..2817e3cbc7 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -99,7 +99,6 @@ public class TvShowsController : BaseJellyfinApiController } var options = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var result = _tvSeriesManager.GetNextUp( @@ -161,7 +160,6 @@ public class TvShowsController : BaseJellyfinApiController var parentIdGuid = parentId ?? Guid.Empty; var options = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user) @@ -231,7 +229,6 @@ public class TvShowsController : BaseJellyfinApiController List<BaseItem> episodes; var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey(); @@ -360,7 +357,6 @@ public class TvShowsController : BaseJellyfinApiController }); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var returnItems = _dtoService.GetBaseItemDtos(seasons, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 0e04beb14e..37ac8d4fc5 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -94,7 +94,7 @@ public class UserLibraryController : BaseJellyfinApiController await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -133,7 +133,7 @@ public class UserLibraryController : BaseJellyfinApiController } var item = _libraryManager.GetUserRootFolder(); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); return _dtoService.GetBaseItemDto(item, dtoOptions, user); } @@ -180,7 +180,7 @@ public class UserLibraryController : BaseJellyfinApiController } var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray(); return new QueryResult<BaseItemDto>(dtos); @@ -422,7 +422,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); if (item is IHasTrailers hasTrailers) { var trailers = hasTrailers.LocalTrailers; @@ -478,7 +478,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); return Ok(item .GetExtras() @@ -549,7 +549,6 @@ public class UserLibraryController : BaseJellyfinApiController } var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); var list = _userViewManager.GetLatestItems( diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 64b2dffb32..ed4bba2bb1 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -86,7 +86,7 @@ public class UserViewsController : BaseJellyfinApiController var folders = _userViewManager.GetUserViews(query); - var dtoOptions = new DtoOptions().AddClientFields(User); + var dtoOptions = new DtoOptions(); dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId]; var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user)); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 97f3239bbc..e7c6f23ce5 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -111,7 +111,6 @@ public class VideosController : BaseJellyfinApiController } var dtoOptions = new DtoOptions(); - dtoOptions = dtoOptions.AddClientFields(User); BaseItemDto[] items; if (item is Video video) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 5495f60d88..685334a9f0 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -89,7 +89,6 @@ public class YearsController : BaseJellyfinApiController { userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } - .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); User? user = userId.IsNullOrEmpty() @@ -182,8 +181,7 @@ public class YearsController : BaseJellyfinApiController return NotFound(); } - var dtoOptions = new DtoOptions() - .AddClientFields(User); + var dtoOptions = new DtoOptions(); if (!userId.IsNullOrEmpty()) { diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index f919a4707d..9c24be82ea 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -1,10 +1,6 @@ -using System; using System.Collections.Generic; -using System.Security.Claims; -using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; namespace Jellyfin.Api.Extensions; @@ -13,55 +9,6 @@ namespace Jellyfin.Api.Extensions; /// </summary> public static class DtoExtensions { - /// <summary> - /// Add additional fields depending on client. - /// </summary> - /// <remarks> - /// Use in place of GetDtoOptions. - /// Legacy order: 2. - /// </remarks> - /// <param name="dtoOptions">DtoOptions object.</param> - /// <param name="user">Current claims principal.</param> - /// <returns>Modified DtoOptions object.</returns> - internal static DtoOptions AddClientFields( - this DtoOptions dtoOptions, ClaimsPrincipal user) - { - string? client = user.GetClient(); - - // No client in claim - if (string.IsNullOrEmpty(client)) - { - return dtoOptions; - } - - if (!dtoOptions.ContainsField(ItemFields.RecursiveItemCount)) - { - if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) || - client.Contains("wmc", StringComparison.OrdinalIgnoreCase) || - client.Contains("media center", StringComparison.OrdinalIgnoreCase) || - client.Contains("classic", StringComparison.OrdinalIgnoreCase)) - { - dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount]; - } - } - - if (!dtoOptions.ContainsField(ItemFields.ChildCount)) - { - if (client.Contains("kodi", StringComparison.OrdinalIgnoreCase) || - client.Contains("wmc", StringComparison.OrdinalIgnoreCase) || - client.Contains("media center", StringComparison.OrdinalIgnoreCase) || - client.Contains("classic", StringComparison.OrdinalIgnoreCase) || - client.Contains("roku", StringComparison.OrdinalIgnoreCase) || - client.Contains("samsung", StringComparison.OrdinalIgnoreCase) || - client.Contains("androidtv", StringComparison.OrdinalIgnoreCase)) - { - dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount]; - } - } - - return dtoOptions; - } - /// <summary> /// Add additional DtoOptions. /// </summary> From 691c194152df841e4ebd753b1c3a0d75e5d13e79 Mon Sep 17 00:00:00 2001 From: Hasan Abdulaal <hassan.b2018@gmail.com> Date: Tue, 2 Dec 2025 06:11:42 -0500 Subject: [PATCH 036/390] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index a92148cafe..24ed116f39 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -16,7 +16,7 @@ "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "إستئناف المشاهدة", + "HeaderContinueWatching": "أكمل المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", From 7425a493eef35078b447d1dff984b4e9ba7f0088 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 05:30:25 +0000 Subject: [PATCH 037/390] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 8 ++++---- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 4 ++-- .github/workflows/ci-tests.yml | 2 +- .github/workflows/commands.yml | 4 ++-- .github/workflows/issue-stale.yml | 2 +- .github/workflows/issue-template-check.yml | 2 +- .github/workflows/pull-request-stale.yaml | 2 +- .github/workflows/release-bump-version.yaml | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 2918bd6a7c..1a0e8e8d7a 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -20,18 +20,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 + uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 + uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 + uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 298c21d7fc..8a755a3172 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -11,7 +11,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -40,7 +40,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index e1ee6ebe59..0a391dbe1b 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -16,7 +16,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -41,7 +41,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index b2ac7a56e6..5b76d2b6b0 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ matrix.os }}" steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 4ec92d42ee..0d3e09d1a1 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: pull in script - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: jellyfin/jellyfin-triage-script - name: install python diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index db22848c3f..cb535297e0 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 50453d1121..8be48b5c3a 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -10,7 +10,7 @@ jobs: issues: write steps: - name: pull in script - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: jellyfin/jellyfin-triage-script - name: install python diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index 223ffc590b..0d74e643e2 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml index fa4025de50..d39d2cb9c3 100644 --- a/.github/workflows/release-bump-version.yaml +++ b/.github/workflows/release-bump-version.yaml @@ -33,7 +33,7 @@ jobs: yq-version: v4.9.8 - name: Checkout Repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ env.TAG_BRANCH }} From 69c98af9f9223797a41ad09a5d57f1d955bc5429 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Wed, 3 Dec 2025 09:45:50 -0500 Subject: [PATCH 038/390] Add CPU to issue template --- .github/ISSUE_TEMPLATE/issue report.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 2ed8b05122..ea3cd389e0 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.4 - 10.11.3 - 10.11.2 - 10.11.1 @@ -139,13 +140,14 @@ body: - **FFmpeg Version**: [e.g. 5.1.2-Jellyfin] - **Playback**: [Direct Play, Remux, Direct Stream, Transcode] - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.] + - **CPU Model**: [e.g. AMD Ryzen 5 9600X, Intel Core i7-8565U, etc.] - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.] - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.] - **Reverse Proxy**: [e.g. none, nginx, apache, etc.] - **Base URL**: [e.g. none, yes: /example] - **Networking**: [e.g. Host, Bridge/NAT] - - **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD] - - **Media Storage**: [e.g. Local HDD, SMB Share] + - **Jellyfin Data Storage & Filesystem**: [e.g. local SATA SSD - ext4, local HDD - NTFS] + - **Media Storage & Filesystem**: [e.g. Local HDD - ext4, SMB Share] - **External Integrations**: [e.g. Jellystat, Jellyseerr] value: | - OS: @@ -156,13 +158,14 @@ body: - FFmpeg Version: - Playback Method: - Hardware Acceleration: + - CPU Model: - GPU Model: - Plugins: - Reverse Proxy: - Base URL: - Networking: - - Jellyfin Data Storage: - - Media Storage: + - Jellyfin Data Storage & Filesystem: + - Media Storage & Filesystem: - External Integrations: render: markdown validations: From e4daaf0d8330ab1e8abadcb927b03b5ded08895a Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:17 -0500 Subject: [PATCH 039/390] Backport pull request #15548 from jellyfin/release-10.11.z Fix NullReferenceException in filesystem path comparison Original-merge: 5ae444d96d473ba42c4a812c3f366b0faa6ebef4 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index fad97344b5..4d68cb4444 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -497,8 +497,17 @@ namespace Emby.Server.Implementations.IO /// <inheritdoc /> public virtual bool AreEqual(string path1, string path2) { - return Path.TrimEndingDirectorySeparator(path1).Equals( - Path.TrimEndingDirectorySeparator(path2), + if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2)) + { + return false; + } + + var normalized1 = Path.TrimEndingDirectorySeparator(path1); + var normalized2 = Path.TrimEndingDirectorySeparator(path2); + + return string.Equals( + normalized1, + normalized2, _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } From 8cd6ef37c4a4220ddca502beb61e979767d49344 Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:18 -0500 Subject: [PATCH 040/390] Backport pull request #15556 from jellyfin/release-10.11.z Prevent copying HDR streams when only SDR is supported Original-merge: 1e7e46cb8212385f86564b92d111ad80464f45d0 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a1d8915353..915c787f29 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2378,6 +2378,13 @@ namespace MediaBrowser.Controller.MediaEncoding var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); + // If SDR is the only supported range, we should not copy any of the HDR streams. + // All the following copy check assumes at least one HDR format is supported. + if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR) + { + return false; + } + // If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it. if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI) { From 7d2e4cd8170fd3369e2861f572f54c104ec754fb Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:19 -0500 Subject: [PATCH 041/390] Backport pull request #15557 from jellyfin/release-10.11.z Restrict first video frame probing to file protocol Original-merge: ee7ad83427ed30aa095896ff3577bb946f3d1c02 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index b7fef842b3..73c5b88c8b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; - if (!isAudio && _proberSupportsFirstVideoFrame) + if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame) { args += " -show_frames -only_first_vframe"; } From 3460d1de3cd0cc710a411640a26c9f43f39f7728 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:20 -0500 Subject: [PATCH 042/390] Backport pull request #15563 from jellyfin/release-10.11.z Save item to database before providers run to prevent FK errors Original-merge: c491a918c21025b105afba4b6d72a24372aac505 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Providers/Manager/MetadataService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f220ec4a14..a2102ca9cd 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -151,9 +151,9 @@ namespace MediaBrowser.Providers.Manager .ConfigureAwait(false); updateType |= beforeSaveResult; - if (!isFirstRefresh) + if (isFirstRefresh) { - updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } // Next run metadata providers From 4f020a947ae7e2765a8b04bb56ffe11caf24aeb8 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:22 -0500 Subject: [PATCH 043/390] Backport pull request #15564 from jellyfin/release-10.11.z Fix locked fields not saving Original-merge: 0ee81e87be58072e21a3bc69fc1d1d0fbc83974a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 2c18ce69ac..f4bb94349d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -618,12 +618,18 @@ public sealed class BaseItemRepository { context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete(); if (entity.Images is { Count: > 0 }) { context.BaseItemImageInfos.AddRange(entity.Images); } + if (entity.LockedFields is { Count: > 0 }) + { + context.BaseItemMetadataFields.AddRange(entity.LockedFields); + } + context.BaseItems.Attach(entity).State = EntityState.Modified; } } From 5d4627858418722c85d76d27b4ba982f209505f2 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:23 -0500 Subject: [PATCH 044/390] Backport pull request #15568 from jellyfin/release-10.11.z Fix ResolveLinkTarget crashing on exFAT drives Original-merge: fbb9a0b2c7c5afbc56be76a4eb11a1045f0ab0f0 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../IO/FileSystemHelper.cs | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 3e390ca428..44b7fadf5e 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -63,6 +63,29 @@ public static class FileSystemHelper } } + /// <summary> + /// Resolves a single link hop for the specified path. + /// </summary> + /// <remarks> + /// Returns <c>null</c> if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT). + /// </remarks> + /// <param name="path">The file path to resolve.</param> + /// <returns> + /// A <see cref="FileInfo"/> representing the next link target if the path is a link; otherwise, <c>null</c>. + /// </returns> + private static FileInfo? Resolve(string path) + { + try + { + return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo; + } + catch (IOException) + { + // Filesystem doesn't support links (e.g., exFAT). + return null; + } + } + /// <summary> /// Gets the target of the specified file link. /// </summary> @@ -84,23 +107,26 @@ public static class FileSystemHelper if (!returnFinalTarget) { - return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + return Resolve(linkPath); } - if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) - { - return null; - } - - if (!targetInfo.Exists) + var targetInfo = Resolve(linkPath); + if (targetInfo is null || !targetInfo.Exists) { return targetInfo; } var currentPath = targetInfo.FullName; var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath }; - while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + + while (true) { + var linkInfo = Resolve(currentPath); + if (linkInfo is null) + { + break; + } + var targetPath = linkInfo.FullName; // If an infinite loop is detected, return the file info for the From ebcfed83c427b1d7c1429dc0316b8f8eaf179697 Mon Sep 17 00:00:00 2001 From: QuintonQu <qu.ziyuan.cn@gmail.com> Date: Wed, 3 Dec 2025 14:04:24 -0500 Subject: [PATCH 045/390] Backport pull request #15582 from jellyfin/release-10.11.z Add hidden file check in BdInfoDirectoryInfo.cs. Original-merge: 29b3aa854310c150e23ec27a41d912fd6fde3c7d Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 7c0be5a9f6..dc20a6d631 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using BDInfo.IO; @@ -58,6 +59,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo } } + private static bool IsHidden(ReadOnlySpan<char> name) => name.StartsWith('.'); + /// <summary> /// Gets the directories. /// </summary> @@ -65,6 +68,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo public IDirectoryInfo[] GetDirectories() { return _fileSystem.GetDirectories(_impl.FullName) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoDirectoryInfo(_fileSystem, x)) .ToArray(); } @@ -76,6 +80,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo public IFileInfo[] GetFiles() { return _fileSystem.GetFiles(_impl.FullName) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } @@ -88,6 +93,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo public IFileInfo[] GetFiles(string searchPattern) { return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } @@ -105,6 +111,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo new[] { searchPattern }, false, searchOption == SearchOption.AllDirectories) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } From 70dcf3f7b30df007661a252a3bb1790a6b56b263 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:25 -0500 Subject: [PATCH 046/390] Backport pull request #15594 from jellyfin/release-10.11.z Fix isMovie filter logic Original-merge: 94f3725208caa030910b62b798ad2f78608d6fd6 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 17 ++++++++--------- MediaBrowser.Controller/Entities/Folder.cs | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f4bb94349d..84168291a8 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1653,19 +1653,18 @@ public sealed class BaseItemRepository var tags = filter.Tags.ToList(); var excludeTags = filter.ExcludeTags.ToList(); - if (filter.IsMovie == true) + if (filter.IsMovie.HasValue) { - if (filter.IncludeItemTypes.Length == 0 - || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) - || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + var shouldIncludeAllMovieTypes = filter.IsMovie.Value + && (filter.IncludeItemTypes.Length == 0 + || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) + || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)); + + if (!shouldIncludeAllMovieTypes) { - baseQuery = baseQuery.Where(e => e.IsMovie); + baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value); } } - else if (filter.IsMovie.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); - } if (filter.IsSeries.HasValue) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 151b957fe9..59a967725f 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1409,7 +1409,7 @@ namespace MediaBrowser.Controller.Entities if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0)) { realChildren = realChildren - .OrderBy(e => e.ProductionYear ?? int.MaxValue) + .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue) .ToArray(); } From deb81eae1081edc0797fe17283f838da9c7e8a21 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Wed, 3 Dec 2025 14:04:27 -0500 Subject: [PATCH 047/390] Backport pull request #15670 from jellyfin/release-10.11.z Fix the empty output of trickplay on RK3576 Original-merge: 98d1d0cb35a56eadfde335916a937940faf75a23 Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../MediaEncoding/EncodingHelper.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 915c787f29..843590a1f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5949,28 +5949,37 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; - var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts + var outFormat = doOclTonemap ? "p010" : "nv12"; var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH)); if (!hasSubs || doRkVppTranspose || !isFullAfbcPipeline - || !string.IsNullOrEmpty(doScaling)) + || doScaling) { + var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f); + // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it - if (!string.IsNullOrEmpty(doScaling) - && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) + if (doScaling && !isScaleRatioSupported) { // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format. // Use NV15 instead of P010 to avoid the issue. // SDR inputs are using BGRA formats already which is not affected. - var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat; + var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat); var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1"; mainFilters.Add(hwScaleFilterFirstPass); } + // The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input. + // Use 2pass here to enable RGA output of full-range YUV in the 2nd pass. + if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling)) + { + var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1"; + mainFilters.Add(hwScaleFilterFirstPass); + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { hwScaleFilter += $":transpose={transposeDir}"; From 6e74be0d46f409b7b63f02a29cbbbd572f40bd32 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Wed, 3 Dec 2025 14:04:28 -0500 Subject: [PATCH 048/390] Backport pull request #15672 from jellyfin/release-10.11.z Cache OpenApi document generation Original-merge: 8cd56521570992d8587db5e1f80d4cb826537f31 Merged-by: anthonylavado <anthony@lavado.ca> Backported-by: Bond_009 <bond.009@outlook.com> --- .../ApiServiceCollectionExtensions.cs | 5 +- .../Filters/CachingOpenApiProvider.cs | 89 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/Filters/CachingOpenApiProvider.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 08c1a5065b..04dd19eda6 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -33,9 +33,11 @@ using Microsoft.AspNetCore.Builder; 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 Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; @@ -259,7 +261,8 @@ namespace Jellyfin.Server.Extensions c.OperationFilter<FileRequestFilter>(); c.OperationFilter<ParameterObsoleteFilter>(); c.DocumentFilter<AdditionalModelFilter>(); - }); + }) + .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>()); } private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs new file mode 100644 index 0000000000..4169f2fb31 --- /dev/null +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// <summary> +/// OpenApi provider with caching. +/// </summary> +internal sealed class CachingOpenApiProvider : ISwaggerProvider +{ + private const string CacheKey = "openapi.json"; + + private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) }; + private static readonly SemaphoreSlim _lock = new(1, 1); + private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1); + + private readonly IMemoryCache _memoryCache; + private readonly SwaggerGenerator _swaggerGenerator; + private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions; + + /// <summary> + /// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class. + /// </summary> + /// <param name="optionsAccessor">The options accessor.</param> + /// <param name="apiDescriptionsProvider">The api descriptions provider.</param> + /// <param name="schemaGenerator">The schema generator.</param> + /// <param name="memoryCache">The memory cache.</param> + public CachingOpenApiProvider( + IOptions<SwaggerGeneratorOptions> optionsAccessor, + IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + ISchemaGenerator schemaGenerator, + IMemoryCache memoryCache) + { + _swaggerGeneratorOptions = optionsAccessor.Value; + _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); + _memoryCache = memoryCache; + } + + /// <inheritdoc /> + public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null) + { + if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null) + { + return AdjustDocument(openApiDocument, host, basePath); + } + + var acquired = _lock.Wait(_lockTimeout); + try + { + if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null) + { + return AdjustDocument(openApiDocument, host, basePath); + } + + if (!acquired) + { + throw new InvalidOperationException("OpenApi document is generating"); + } + + openApiDocument = _swaggerGenerator.GetSwagger(documentName); + _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); + return AdjustDocument(openApiDocument, host, basePath); + } + finally + { + if (acquired) + { + _lock.Release(); + } + } + } + + private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath) + { + document.Servers = _swaggerGeneratorOptions.Servers.Count != 0 + ? _swaggerGeneratorOptions.Servers + : string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath) + ? [] + : [new OpenApiServer { Url = $"{host}{basePath}" }]; + + return document; + } +} From 873f1d9e8344d6c205e15d1be1c3af5514351a54 Mon Sep 17 00:00:00 2001 From: Furqaan Dawood <furqaanfurqaan11@gmail.com> Date: Thu, 4 Dec 2025 09:41:47 -0500 Subject: [PATCH 049/390] Added translation using Weblate (Swahili) --- Emby.Server.Implementations/Localization/Core/sw.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/sw.json diff --git a/Emby.Server.Implementations/Localization/Core/sw.json b/Emby.Server.Implementations/Localization/Core/sw.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/sw.json @@ -0,0 +1 @@ +{} From a25b48b1519d38c774d5ceb7b514cdf7811094e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:58:06 +0000 Subject: [PATCH 050/390] Update CI dependencies --- .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 1a0e8e8d7a..5a6cccda0d 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 + uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 5b76d2b6b0..f70243221d 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@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0 + uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From 8fd59d6f336a95782a13b0440a6cf5108e58a1f8 Mon Sep 17 00:00:00 2001 From: audrey-inglish <112636119+audrey-inglish@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:43:37 -0700 Subject: [PATCH 051/390] Merge pull request #14879 from audrey-inglish/master Fix: normalize punctuation when computing CleanName so searches without punctuation match (closes #1674) --- .../Item/BaseItemRepository.cs | 44 ++++++- .../Migrations/Routines/RefreshCleanNames.cs | 105 +++++++++++++++++ .../Data/SearchPunctuationTests.cs | 109 ++++++++++++++++++ 3 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 84168291a8..57d874e59f 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1373,14 +1373,54 @@ public sealed class BaseItemRepository } } - private string GetCleanValue(string value) + /// <summary> + /// Gets the clean value for search and sorting purposes. + /// </summary> + /// <param name="value">The value to clean.</param> + /// <returns>The cleaned value.</returns> + public static string GetCleanValue(string value) { if (string.IsNullOrWhiteSpace(value)) { return value; } - return value.RemoveDiacritics().ToLowerInvariant(); + var noDiacritics = value.RemoveDiacritics(); + + // Build a string where any punctuation or symbol is treated as a separator (space). + var sb = new StringBuilder(noDiacritics.Length); + var previousWasSpace = false; + foreach (var ch in noDiacritics) + { + char outCh; + if (char.IsLetterOrDigit(ch) || char.IsWhiteSpace(ch)) + { + outCh = ch; + } + else + { + outCh = ' '; + } + + // normalize any whitespace character to a single ASCII space. + if (char.IsWhiteSpace(outCh)) + { + if (!previousWasSpace) + { + sb.Append(' '); + previousWasSpace = true; + } + } + else + { + sb.Append(outCh); + previousWasSpace = false; + } + } + + // trim leading/trailing spaces that may have been added. + var collapsed = sb.ToString().Trim(); + return collapsed.ToLowerInvariant(); } private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags) diff --git a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs new file mode 100644 index 0000000000..eadabf6776 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; +using Jellyfin.Server.Implementations.Item; +using Jellyfin.Server.ServerSetupApp; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// <summary> +/// Migration to refresh CleanName values for all library items. +/// </summary> +[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))] +[JellyfinMigrationBackup(JellyfinDb = true)] +public class RefreshCleanNames : IAsyncMigrationRoutine +{ + private readonly IStartupLogger<RefreshCleanNames> _logger; + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; + + /// <summary> + /// Initializes a new instance of the <see cref="RefreshCleanNames"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param> + public RefreshCleanNames( + IStartupLogger<RefreshCleanNames> logger, + IDbContextFactory<JellyfinDbContext> dbProvider) + { + _logger = logger; + _dbProvider = dbProvider; + } + + /// <inheritdoc /> + public async Task PerformAsync(CancellationToken cancellationToken) + { + const int Limit = 1000; + int itemCount = 0; + + var sw = Stopwatch.StartNew(); + + using var context = _dbProvider.CreateDbContext(); + var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.Name)); + _logger.LogInformation("Refreshing CleanName for {Count} library items", records); + + var processedInPartition = 0; + + await foreach (var item in context.BaseItems + .Where(b => !string.IsNullOrEmpty(b.Name)) + .OrderBy(e => e.Id) + .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed)) + .PartitionEagerAsync(Limit, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + try + { + var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : BaseItemRepository.GetCleanValue(item.Name); + if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal)) + { + _logger.LogDebug( + "Updating CleanName for item {Id}: '{OldValue}' -> '{NewValue}'", + item.Id, + item.CleanName, + newCleanName); + item.CleanName = newCleanName; + itemCount++; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update CleanName for item {Id} ({Name})", item.Id, item.Name); + } + + processedInPartition++; + + if (processedInPartition >= Limit) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + // Clear tracked entities to avoid memory growth across partitions + context.ChangeTracker.Clear(); + processedInPartition = 0; + } + } + + // Save any remaining changes after the loop + if (processedInPartition > 0) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + context.ChangeTracker.Clear(); + } + + _logger.LogInformation( + "Refreshed CleanName for {UpdatedCount} out of {TotalCount} items in {Time}", + itemCount, + records, + sw.Elapsed); + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs new file mode 100644 index 0000000000..8fbccd8019 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Data/SearchPunctuationTests.cs @@ -0,0 +1,109 @@ +using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Controller.Entities.TV; +using Microsoft.Extensions.Configuration; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Data +{ + public class SearchPunctuationTests + { + private readonly IFixture _fixture; + private readonly BaseItemRepository _repo; + + public SearchPunctuationTests() + { + var appHost = new Mock<MediaBrowser.Controller.IServerApplicationHost>(); + appHost.Setup(x => x.ExpandVirtualPath(It.IsAny<string>())) + .Returns((string x) => x); + appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>())) + .Returns((string x) => x); + + var configSection = new Mock<IConfigurationSection>(); + configSection + .SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)]) + .Returns("0"); + var config = new Mock<IConfiguration>(); + config + .Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey))) + .Returns(configSection.Object); + + _fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + _fixture.Inject(appHost.Object); + _fixture.Inject(config.Object); + + _repo = _fixture.Create<BaseItemRepository>(); + } + + [Fact] + public void CleanName_keeps_punctuation_and_search_without_punctuation_passes() + { + var series = new Series + { + Id = Guid.NewGuid(), + Name = "Mr. Robot" + }; + + series.SortName = "Mr. Robot"; + + var entity = _repo.Map(series); + Assert.Equal("mr robot", entity.CleanName); + + var searchTerm = "Mr Robot".ToLowerInvariant(); + + Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("Spider-Man: Homecoming", "spider man homecoming")] + [InlineData("Beyoncé — Live!", "beyonce live")] + [InlineData("Hello, World!", "hello world")] + [InlineData("(The) Good, the Bad & the Ugly", "the good the bad the ugly")] + [InlineData("Wall-E", "wall e")] + [InlineData("No. 1: The Beginning", "no 1 the beginning")] + [InlineData("Café-au-lait", "cafe au lait")] + public void CleanName_normalizes_various_punctuation(string title, string expectedClean) + { + var series = new Series + { + Id = Guid.NewGuid(), + Name = title + }; + + series.SortName = title; + + var entity = _repo.Map(series); + + Assert.Equal(expectedClean, entity.CleanName); + + // Ensure a search term without punctuation would match + var searchTerm = expectedClean; + Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("Face/Off", "face off")] + [InlineData("V/H/S", "v h s")] + public void CleanName_normalizes_titles_withslashes(string title, string expectedClean) + { + var series = new Series + { + Id = Guid.NewGuid(), + Name = title + }; + + series.SortName = title; + + var entity = _repo.Map(series); + + Assert.Equal(expectedClean, entity.CleanName); + + // Ensure a search term without punctuation would match + var searchTerm = expectedClean; + Assert.Contains(searchTerm, entity.CleanName ?? string.Empty, StringComparison.OrdinalIgnoreCase); + } + } +} From c3a8734adf00c85ff7676d2a7caad1f5aa8cd01a Mon Sep 17 00:00:00 2001 From: Mark Cilia Vincenti <markciliavincenti@gmail.com> Date: Tue, 9 Dec 2025 05:01:12 +0100 Subject: [PATCH 052/390] Locking cleaning (#15713) --- Directory.Packages.props | 2 +- .../Filters/CachingOpenApiProvider.cs | 32 +++++++------------ .../MediaBrowser.Common.csproj | 2 +- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 210cbd35d6..1d18a705cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ </PropertyGroup> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <ItemGroup Label="Package Dependencies"> - <PackageVersion Include="AsyncKeyedLock" Version="7.1.7" /> + <PackageVersion Include="AsyncKeyedLock" Version="7.1.8" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" /> diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index 4169f2fb31..b560ec50ef 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -1,5 +1,5 @@ using System; -using System.Threading; +using AsyncKeyedLock; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -17,7 +17,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider private const string CacheKey = "openapi.json"; private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) }; - private static readonly SemaphoreSlim _lock = new(1, 1); + private static readonly AsyncNonKeyedLocker _lock = new(1); private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1); private readonly IMemoryCache _memoryCache; @@ -50,30 +50,20 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider return AdjustDocument(openApiDocument, host, basePath); } - var acquired = _lock.Wait(_lockTimeout); - try + using var acquired = _lock.LockOrNull(_lockTimeout); + if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null) { - if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null) - { - return AdjustDocument(openApiDocument, host, basePath); - } - - if (!acquired) - { - throw new InvalidOperationException("OpenApi document is generating"); - } - - openApiDocument = _swaggerGenerator.GetSwagger(documentName); - _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); return AdjustDocument(openApiDocument, host, basePath); } - finally + + if (acquired is null) { - if (acquired) - { - _lock.Release(); - } + throw new InvalidOperationException("OpenApi document is generating"); } + + openApiDocument = _swaggerGenerator.GetSwagger(documentName); + _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); + return AdjustDocument(openApiDocument, host, basePath); } private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath) diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 9af13b0a72..5f15f845c5 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> From 0b3d6676d1dc78f38cd17c04ecafe2196a291199 Mon Sep 17 00:00:00 2001 From: Cody Robibero <cody@robibe.ro> Date: Mon, 8 Dec 2025 21:01:32 -0700 Subject: [PATCH 053/390] Add ability to sort and filter activity log entries (#15583) --- .../Controllers/ActivityLogController.cs | 76 +++++- Jellyfin.Data/Enums/ActivityLogSortBy.cs | 49 ++++ Jellyfin.Data/Queries/ActivityLogQuery.cs | 69 +++++- .../Activity/ActivityManager.cs | 233 ++++++++++++------ .../Activity/IActivityManager.cs | 43 ++-- 5 files changed, 364 insertions(+), 106 deletions(-) create mode 100644 Jellyfin.Data/Enums/ActivityLogSortBy.cs diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index a19a203b51..d5f2627739 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -1,13 +1,16 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Api.Constants; +using Jellyfin.Data.Enums; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Api; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers; @@ -32,10 +35,19 @@ public class ActivityLogController : BaseJellyfinApiController /// <summary> /// Gets activity log entries. /// </summary> - /// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> - /// <param name="limit">Optional. The maximum number of records to return.</param> - /// <param name="minDate">Optional. The minimum date. Format = ISO.</param> - /// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param> + /// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param> + /// <param name="limit">The maximum number of records to return.</param> + /// <param name="minDate">The minimum date.</param> + /// <param name="hasUserId">Filter log entries if it has user id, or not.</param> + /// <param name="name">Filter by name.</param> + /// <param name="overview">Filter by overview.</param> + /// <param name="shortOverview">Filter by short overview.</param> + /// <param name="type">Filter by type.</param> + /// <param name="itemId">Filter by item id.</param> + /// <param name="username">Filter by username.</param> + /// <param name="severity">Filter by log severity.</param> + /// <param name="sortBy">Specify one or more sort orders. Format: SortBy=Name,Type.</param> + /// <param name="sortOrder">Sort Order..</param> /// <response code="200">Activity log returned.</response> /// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns> [HttpGet("Entries")] @@ -44,14 +56,60 @@ public class ActivityLogController : BaseJellyfinApiController [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] DateTime? minDate, - [FromQuery] bool? hasUserId) + [FromQuery] bool? hasUserId, + [FromQuery] string? name, + [FromQuery] string? overview, + [FromQuery] string? shortOverview, + [FromQuery] string? type, + [FromQuery] Guid? itemId, + [FromQuery] string? username, + [FromQuery] LogLevel? severity, + [FromQuery] ActivityLogSortBy[]? sortBy, + [FromQuery] SortOrder[]? sortOrder) { - return await _activityManager.GetPagedResultAsync(new ActivityLogQuery + var query = new ActivityLogQuery { Skip = startIndex, Limit = limit, MinDate = minDate, - HasUserId = hasUserId - }).ConfigureAwait(false); + HasUserId = hasUserId, + Name = name, + Overview = overview, + ShortOverview = shortOverview, + Type = type, + ItemId = itemId, + Username = username, + Severity = severity, + OrderBy = GetOrderBy(sortBy ?? [], sortOrder ?? []), + }; + + return await _activityManager.GetPagedResultAsync(query).ConfigureAwait(false); + } + + private static (ActivityLogSortBy SortBy, SortOrder SortOrder)[] GetOrderBy( + IReadOnlyList<ActivityLogSortBy> sortBy, + IReadOnlyList<SortOrder> requestedSortOrder) + { + if (sortBy.Count == 0) + { + return []; + } + + var result = new (ActivityLogSortBy, SortOrder)[sortBy.Count]; + var i = 0; + for (; i < requestedSortOrder.Count; i++) + { + result[i] = (sortBy[i], requestedSortOrder[i]); + } + + // Add remaining elements with the first specified SortOrder + // or the default one if no SortOrders are specified + var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending; + for (; i < sortBy.Count; i++) + { + result[i] = (sortBy[i], order); + } + + return result; } } diff --git a/Jellyfin.Data/Enums/ActivityLogSortBy.cs b/Jellyfin.Data/Enums/ActivityLogSortBy.cs new file mode 100644 index 0000000000..d6d44e8c07 --- /dev/null +++ b/Jellyfin.Data/Enums/ActivityLogSortBy.cs @@ -0,0 +1,49 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// Activity log sorting options. +/// </summary> +public enum ActivityLogSortBy +{ + /// <summary> + /// Sort by name. + /// </summary> + Name = 0, + + /// <summary> + /// Sort by overview. + /// </summary> + Overiew = 1, + + /// <summary> + /// Sort by short overview. + /// </summary> + ShortOverview = 2, + + /// <summary> + /// Sort by type. + /// </summary> + Type = 3, + + /* + /// <summary> + /// Sort by item name. + /// </summary> + Item = 4, + */ + + /// <summary> + /// Sort by date. + /// </summary> + DateCreated = 5, + + /// <summary> + /// Sort by username. + /// </summary> + Username = 6, + + /// <summary> + /// Sort by severity. + /// </summary> + LogSeverity = 7 +} diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs index f1af099d3c..95c52f8705 100644 --- a/Jellyfin.Data/Queries/ActivityLogQuery.cs +++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs @@ -1,20 +1,63 @@ using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; +using Microsoft.Extensions.Logging; -namespace Jellyfin.Data.Queries +namespace Jellyfin.Data.Queries; + +/// <summary> +/// A class representing a query to the activity logs. +/// </summary> +public class ActivityLogQuery : PaginatedQuery { /// <summary> - /// A class representing a query to the activity logs. + /// Gets or sets a value indicating whether to take entries with a user id. /// </summary> - public class ActivityLogQuery : PaginatedQuery - { - /// <summary> - /// Gets or sets a value indicating whether to take entries with a user id. - /// </summary> - public bool? HasUserId { get; set; } + public bool? HasUserId { get; set; } - /// <summary> - /// Gets or sets the minimum date to query for. - /// </summary> - public DateTime? MinDate { get; set; } - } + /// <summary> + /// Gets or sets the minimum date to query for. + /// </summary> + public DateTime? MinDate { get; set; } + + /// <summary> + /// Gets or sets the name filter. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the overview filter. + /// </summary> + public string? Overview { get; set; } + + /// <summary> + /// Gets or sets the short overview filter. + /// </summary> + public string? ShortOverview { get; set; } + + /// <summary> + /// Gets or sets the type filter. + /// </summary> + public string? Type { get; set; } + + /// <summary> + /// Gets or sets the item filter. + /// </summary> + public Guid? ItemId { get; set; } + + /// <summary> + /// Gets or sets the username filter. + /// </summary> + public string? Username { get; set; } + + /// <summary> + /// Gets or sets the log level filter. + /// </summary> + public LogLevel? Severity { get; set; } + + /// <summary> + /// Gets or sets the result ordering. + /// </summary> + public IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? OrderBy { get; set; } } diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 8d492f7cd7..7ee573f538 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -1,103 +1,198 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Extensions; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; -namespace Jellyfin.Server.Implementations.Activity +namespace Jellyfin.Server.Implementations.Activity; + +/// <summary> +/// Manages the storage and retrieval of <see cref="ActivityLog"/> instances. +/// </summary> +public class ActivityManager : IActivityManager { + private readonly IDbContextFactory<JellyfinDbContext> _provider; + /// <summary> - /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances. + /// Initializes a new instance of the <see cref="ActivityManager"/> class. /// </summary> - public class ActivityManager : IActivityManager + /// <param name="provider">The Jellyfin database provider.</param> + public ActivityManager(IDbContextFactory<JellyfinDbContext> provider) { - private readonly IDbContextFactory<JellyfinDbContext> _provider; + _provider = provider; + } - /// <summary> - /// Initializes a new instance of the <see cref="ActivityManager"/> class. - /// </summary> - /// <param name="provider">The Jellyfin database provider.</param> - public ActivityManager(IDbContextFactory<JellyfinDbContext> provider) + /// <inheritdoc/> + public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated; + + /// <inheritdoc/> + public async Task CreateAsync(ActivityLog entry) + { + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - _provider = provider; + dbContext.ActivityLogs.Add(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); } - /// <inheritdoc/> - public event EventHandler<GenericEventArgs<ActivityLogEntry>>? EntryCreated; + EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); + } - /// <inheritdoc/> - public async Task CreateAsync(ActivityLog entry) + /// <inheritdoc/> + public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query) + { + // TODO allow sorting and filtering by item id. Currently not possible because ActivityLog stores the item id as a string. + + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + // TODO switch to LeftJoin in .NET 10. + var entries = from a in dbContext.ActivityLogs + join u in dbContext.Users on a.UserId equals u.Id into ugj + from u in ugj.DefaultIfEmpty() + select new ExpandedActivityLog { ActivityLog = a, Username = u.Username }; + + if (query.HasUserId is not null) { - dbContext.ActivityLogs.Add(entry); - await dbContext.SaveChangesAsync().ConfigureAwait(false); + entries = entries.Where(e => e.ActivityLog.UserId.Equals(default) != query.HasUserId.Value); } - EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); - } - - /// <inheritdoc/> - public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query) - { - var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + if (query.MinDate is not null) { - var entries = dbContext.ActivityLogs - .OrderByDescending(entry => entry.DateCreated) - .Where(entry => query.MinDate == null || entry.DateCreated >= query.MinDate) - .Where(entry => !query.HasUserId.HasValue || entry.UserId.Equals(default) != query.HasUserId.Value); - - return new QueryResult<ActivityLogEntry>( - query.Skip, - await entries.CountAsync().ConfigureAwait(false), - await entries - .Skip(query.Skip ?? 0) - .Take(query.Limit ?? 100) - .Select(entity => new ActivityLogEntry(entity.Name, entity.Type, entity.UserId) - { - Id = entity.Id, - Overview = entity.Overview, - ShortOverview = entity.ShortOverview, - ItemId = entity.ItemId, - Date = entity.DateCreated, - Severity = entity.LogSeverity - }) - .ToListAsync() - .ConfigureAwait(false)); + entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value); } - } - /// <inheritdoc /> - public async Task CleanAsync(DateTime startDate) - { - var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + if (!string.IsNullOrEmpty(query.Name)) { - await dbContext.ActivityLogs - .Where(entry => entry.DateCreated <= startDate) - .ExecuteDeleteAsync() - .ConfigureAwait(false); + entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%")); } - } - private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) - { - return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId) + if (!string.IsNullOrEmpty(query.Overview)) { - Id = entry.Id, - Overview = entry.Overview, - ShortOverview = entry.ShortOverview, - ItemId = entry.ItemId, - Date = entry.DateCreated, - Severity = entry.LogSeverity - }; + entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Overview, $"%{query.Overview}%")); + } + + if (!string.IsNullOrEmpty(query.ShortOverview)) + { + entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.ShortOverview, $"%{query.ShortOverview}%")); + } + + if (!string.IsNullOrEmpty(query.Type)) + { + entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Type, $"%{query.Type}%")); + } + + if (!query.ItemId.IsNullOrEmpty()) + { + var itemId = query.ItemId.Value.ToString("N"); + entries = entries.Where(e => e.ActivityLog.ItemId == itemId); + } + + if (!string.IsNullOrEmpty(query.Username)) + { + entries = entries.Where(e => EF.Functions.Like(e.Username, $"%{query.Username}%")); + } + + if (query.Severity is not null) + { + entries = entries.Where(e => e.ActivityLog.LogSeverity == query.Severity); + } + + return new QueryResult<ActivityLogEntry>( + query.Skip, + await entries.CountAsync().ConfigureAwait(false), + await ApplyOrdering(entries, query.OrderBy) + .Skip(query.Skip ?? 0) + .Take(query.Limit ?? 100) + .Select(entity => new ActivityLogEntry(entity.ActivityLog.Name, entity.ActivityLog.Type, entity.ActivityLog.UserId) + { + Id = entity.ActivityLog.Id, + Overview = entity.ActivityLog.Overview, + ShortOverview = entity.ActivityLog.ShortOverview, + ItemId = entity.ActivityLog.ItemId, + Date = entity.ActivityLog.DateCreated, + Severity = entity.ActivityLog.LogSeverity + }) + .ToListAsync() + .ConfigureAwait(false)); } } + + /// <inheritdoc /> + public async Task CleanAsync(DateTime startDate) + { + var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + await dbContext.ActivityLogs + .Where(entry => entry.DateCreated <= startDate) + .ExecuteDeleteAsync() + .ConfigureAwait(false); + } + } + + private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) + { + return new ActivityLogEntry(entry.Name, entry.Type, entry.UserId) + { + Id = entry.Id, + Overview = entry.Overview, + ShortOverview = entry.ShortOverview, + ItemId = entry.ItemId, + Date = entry.DateCreated, + Severity = entry.LogSeverity + }; + } + + private IOrderedQueryable<ExpandedActivityLog> ApplyOrdering(IQueryable<ExpandedActivityLog> query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting) + { + if (sorting is null || sorting.Count == 0) + { + return query.OrderByDescending(e => e.ActivityLog.DateCreated); + } + + IOrderedQueryable<ExpandedActivityLog> ordered = null!; + + foreach (var (sortBy, sortOrder) in sorting) + { + var orderBy = MapOrderBy(sortBy); + ordered = sortOrder == SortOrder.Ascending + ? (ordered ?? query).OrderBy(orderBy) + : (ordered ?? query).OrderByDescending(orderBy); + } + + return ordered; + } + + private Expression<Func<ExpandedActivityLog, object?>> MapOrderBy(ActivityLogSortBy sortBy) + { + return sortBy switch + { + ActivityLogSortBy.Name => e => e.ActivityLog.Name, + ActivityLogSortBy.Overiew => e => e.ActivityLog.Overview, + ActivityLogSortBy.ShortOverview => e => e.ActivityLog.ShortOverview, + ActivityLogSortBy.Type => e => e.ActivityLog.Type, + ActivityLogSortBy.DateCreated => e => e.ActivityLog.DateCreated, + ActivityLogSortBy.Username => e => e.Username, + ActivityLogSortBy.LogSeverity => e => e.ActivityLog.LogSeverity, + _ => throw new ArgumentOutOfRangeException(nameof(sortBy), sortBy, "Unhandled ActivityLogSortBy") + }; + } + + private class ExpandedActivityLog + { + public ActivityLog ActivityLog { get; set; } = null!; + + public string? Username { get; set; } + } } diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index 95aa567ada..96958e9a73 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Threading.Tasks; using Jellyfin.Data.Events; @@ -7,21 +5,36 @@ using Jellyfin.Data.Queries; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Querying; -namespace MediaBrowser.Model.Activity +namespace MediaBrowser.Model.Activity; + +/// <summary> +/// Interface for the activity manager. +/// </summary> +public interface IActivityManager { - public interface IActivityManager - { - event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; + /// <summary> + /// The event that is triggered when an entity is created. + /// </summary> + event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; - Task CreateAsync(ActivityLog entry); + /// <summary> + /// Create a new activity log entry. + /// </summary> + /// <param name="entry">The entry to create.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + Task CreateAsync(ActivityLog entry); - Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query); + /// <summary> + /// Get a paged list of activity log entries. + /// </summary> + /// <param name="query">The activity log query.</param> + /// <returns>The page of entries.</returns> + Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query); - /// <summary> - /// Remove all activity logs before the specified date. - /// </summary> - /// <param name="startDate">Activity log start date.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - Task CleanAsync(DateTime startDate); - } + /// <summary> + /// Remove all activity logs before the specified date. + /// </summary> + /// <param name="startDate">Activity log start date.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + Task CleanAsync(DateTime startDate); } From f24e80701cca2398b9efea8605bd406ff36f3746 Mon Sep 17 00:00:00 2001 From: xin <simasadves@gmail.com> Date: Tue, 9 Dec 2025 06:06:17 +0200 Subject: [PATCH 054/390] Fix typo in CheckOrCreateMarker exception (#15341) From 8b2a8b94b6361e31eff58078225cf78d8a6c3fb1 Mon Sep 17 00:00:00 2001 From: evan314159 <110177090+evan314159@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:15:46 +0800 Subject: [PATCH 055/390] avoid Take(0) when limit == 0 (#14608) Co-authored-by: Evan <evan@MacBook-Pro.local> --- .../Library/SearchEngine.cs | 2 +- .../TV/TVSeriesManager.cs | 2 +- .../Devices/DeviceManager.cs | 2 +- .../Item/BaseItemRepository.cs | 44 +++++++------------ .../Entities/UserViewBuilder.cs | 2 +- .../Channels/ChannelManager.cs | 9 ++-- src/Jellyfin.LiveTv/LiveTvManager.cs | 4 +- 7 files changed, 26 insertions(+), 39 deletions(-) diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 9d81b835ce..c682118597 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value); } - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count)); } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index ee2e18f735..cd98dbe86e 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -266,7 +266,7 @@ namespace Emby.Server.Implementations.TV items = items.Skip(query.StartIndex.Value); } - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { items = items.Take(query.Limit.Value); } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 51a1186452..bcf348f8c6 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -158,7 +158,7 @@ namespace Jellyfin.Server.Implementations.Devices devices = devices.Skip(query.Skip.Value); } - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { devices = devices.Take(query.Limit.Value); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 57d874e59f..dfe46ef8f6 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -250,7 +250,7 @@ public sealed class BaseItemRepository public QueryResult<BaseItemDto> GetItems(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); - if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) + if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0)) { var returnList = GetItemList(filter); return new QueryResult<BaseItemDto>( @@ -326,7 +326,7 @@ public sealed class BaseItemRepository .OrderByDescending(g => g.MaxDateCreated) .Select(g => g); - if (filter.Limit.HasValue) + if (filter.Limit.HasValue && filter.Limit.Value > 0) { subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value); } @@ -367,7 +367,7 @@ public sealed class BaseItemRepository .OrderByDescending(g => g.LastPlayedDate) .Select(g => g.Key!); - if (filter.Limit.HasValue) + if (filter.Limit.HasValue && filter.Limit.Value > 0) { query = query.Take(filter.Limit.Value); } @@ -425,19 +425,14 @@ public sealed class BaseItemRepository private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) { - if (filter.Limit.HasValue || filter.StartIndex.HasValue) + if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) { - var offset = filter.StartIndex ?? 0; + dbQuery = dbQuery.Skip(filter.StartIndex.Value); + } - if (offset > 0) - { - dbQuery = dbQuery.Skip(offset); - } - - if (filter.Limit.HasValue) - { - dbQuery = dbQuery.Take(filter.Limit.Value); - } + if (filter.Limit.HasValue && filter.Limit.Value > 0) + { + dbQuery = dbQuery.Take(filter.Limit.Value); } return dbQuery; @@ -1190,7 +1185,7 @@ public sealed class BaseItemRepository { ArgumentNullException.ThrowIfNull(filter); - if (!filter.Limit.HasValue) + if (!(filter.Limit.HasValue && filter.Limit.Value > 0)) { filter.EnableTotalRecordCount = false; } @@ -1269,19 +1264,14 @@ public sealed class BaseItemRepository result.TotalRecordCount = query.Count(); } - if (filter.Limit.HasValue || filter.StartIndex.HasValue) + if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) { - var offset = filter.StartIndex ?? 0; + query = query.Skip(filter.StartIndex.Value); + } - if (offset > 0) - { - query = query.Skip(offset); - } - - if (filter.Limit.HasValue) - { - query = query.Take(filter.Limit.Value); - } + if (filter.Limit.HasValue && filter.Limit.Value > 0) + { + query = query.Take(filter.Limit.Value); } IQueryable<BaseItemEntity>? itemCountQuery = null; @@ -1362,7 +1352,7 @@ public sealed class BaseItemRepository private static void PrepareFilterQuery(InternalItemsQuery query) { - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey) { query.Limit = query.Limit.Value + 4; } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 4f9e9261b6..bed7554b19 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); var totalCount = itemsArray.Length; - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray(); } diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 8ee129a575..2b8e5a0a08 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -240,12 +240,9 @@ namespace Jellyfin.LiveTv.Channels var all = channels; var totalCount = all.Count; - if (query.StartIndex.HasValue || query.Limit.HasValue) - { - int startIndex = query.StartIndex ?? 0; - int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex); - all = all.GetRange(startIndex, count); - } + int startIndex = query.StartIndex ?? 0; + int count = (query.Limit ?? 0) > 0 ? Math.Min(query.Limit.Value, totalCount - startIndex) : totalCount - startIndex; + all = all.GetRange(query.StartIndex ?? 0, count); if (query.RefreshLatestChannelItems) { diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 53bc6751fc..1d18ade9dc 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -287,7 +287,7 @@ namespace Jellyfin.LiveTv GenreIds = query.GenreIds }; - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200); } @@ -305,7 +305,7 @@ namespace Jellyfin.LiveTv IEnumerable<BaseItem> programs = orderedPrograms; - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { programs = programs.Take(query.Limit.Value); } From 492ea668412cdeeff7050f952de09946086737f3 Mon Sep 17 00:00:00 2001 From: Mark Cilia Vincenti <markciliavincenti@gmail.com> Date: Tue, 9 Dec 2025 05:16:14 +0100 Subject: [PATCH 056/390] Proper pinning of SkiaSharp to prevent accidental updates (#15736) --- Directory.Packages.props | 8 ++++---- Jellyfin.sln | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1d18a705cb..564223483d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,9 +74,9 @@ <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.2.0" /> <!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 --> - <PackageVersion Include="SkiaSharp" Version="3.116.1" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" /> + <PackageVersion Include="SkiaSharp" Version="[3.116.1]" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" /> @@ -96,4 +96,4 @@ <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" /> <PackageVersion Include="xunit" Version="2.9.3" /> </ItemGroup> -</Project> \ No newline at end of file +</Project> diff --git a/Jellyfin.sln b/Jellyfin.sln index fb1f2a2c20..b0d5a5eb47 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30503.244 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" EndProject @@ -30,6 +30,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{41093F42-C7CC-4D07-956B-6182CBEDE2EC}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + Directory.Packages.props = Directory.Packages.props SharedVersion.cs = SharedVersion.cs EndProjectSection EndProject From 25aef7fabffb0297c2830b23393b99496ddfeba4 Mon Sep 17 00:00:00 2001 From: Tom O'Neill <tom2835@hotmail.com> Date: Tue, 9 Dec 2025 13:18:07 -0500 Subject: [PATCH 057/390] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 09246bd110..350b039c50 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "Favoriete series", "HeaderFavoriteSongs": "Favoriete nummers", "HeaderLiveTV": "Live-tv", - "HeaderNextUp": "Als volgende", + "HeaderNextUp": "Volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", "Inherit": "Erven", From 0f85120c5e9ba2df624e15e1b3c82b2d771f4a0b Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:20:47 -0500 Subject: [PATCH 058/390] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 350b039c50..09246bd110 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "Favoriete series", "HeaderFavoriteSongs": "Favoriete nummers", "HeaderLiveTV": "Live-tv", - "HeaderNextUp": "Volgende", + "HeaderNextUp": "Als volgende", "HeaderRecordingGroups": "Opnamegroepen", "HomeVideos": "Homevideo's", "Inherit": "Erven", From 053cc9406d5b801b0ede26f428ccc4202a6df78f Mon Sep 17 00:00:00 2001 From: Veldermon-rbg <burtjayden3@gmail.com> Date: Tue, 9 Dec 2025 19:29:33 -0500 Subject: [PATCH 059/390] Added translation using Weblate (Maori) --- Emby.Server.Implementations/Localization/Core/mi.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/mi.json diff --git a/Emby.Server.Implementations/Localization/Core/mi.json b/Emby.Server.Implementations/Localization/Core/mi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/mi.json @@ -0,0 +1 @@ +{} From d70e0fe9cf29a647bf67e171d0da9c2b264e8970 Mon Sep 17 00:00:00 2001 From: Veldermon-rbg <burtjayden3@gmail.com> Date: Tue, 9 Dec 2025 19:43:50 -0500 Subject: [PATCH 060/390] Translated using Weblate (Maori) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mi/ --- Emby.Server.Implementations/Localization/Core/mi.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/mi.json b/Emby.Server.Implementations/Localization/Core/mi.json index 0967ef424b..3b20abb369 100644 --- a/Emby.Server.Implementations/Localization/Core/mi.json +++ b/Emby.Server.Implementations/Localization/Core/mi.json @@ -1 +1,9 @@ -{} +{ + "Albums": "Pukaemi", + "AppDeviceValues": "Taupānga: {0}, Pūrere: {1}", + "Application": "Taupānga", + "Artists": "Kaiwaiata", + "AuthenticationSucceededWithUserName": "{0} has been successfully authenticated", + "Books": "Ngā pukapuka", + "CameraImageUploadedFrom": "Kua tuku ake he whakaahua kāmera hou mai i {0}" +} From acb9da6f93829eaef356d3effd1872798c778adb Mon Sep 17 00:00:00 2001 From: Nilesh Patel <nileshp@nilesh.io> Date: Wed, 10 Dec 2025 12:23:05 -0800 Subject: [PATCH 061/390] Add curly brace and parentheses support for parsing attribute values from paths --- .../Library/PathExtensions.cs | 24 +++++++++++++------ .../Library/PathExtensionsTests.cs | 19 +++++++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 21e7079d88..fc63251ad0 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -37,15 +37,25 @@ namespace Emby.Server.Implementations.Library while (attributeIndex > -1 && attributeIndex < maxIndex) { var attributeEnd = attributeIndex + attribute.Length; - if (attributeIndex > 0 - && str[attributeIndex - 1] == '[' - && (str[attributeEnd] == '=' || str[attributeEnd] == '-')) + if (attributeIndex > 0) { - var closingIndex = str[attributeEnd..].IndexOf(']'); - // Must be at least 1 character before the closing bracket. - if (closingIndex > 1) + var attributeOpener = str[attributeIndex - 1]; + var attributeCloser = attributeOpener switch { - return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString(); + '[' => ']', + '(' => ')', + '{' => '}', + _ => '\0' + }; + if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-')) + { + var closingIndex = str[attributeEnd..].IndexOf(attributeCloser); + + // Must be at least 1 character before the closing bracket. + if (closingIndex > 1) + { + return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString(); + } } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index 940e3c2b12..74cd303bab 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -11,21 +11,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")] [InlineData("Superman: Red Son", "imdbid", null)] - [InlineData("Superman: Red Son", "something", null)] [InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] - [InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")] [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")] - [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")] - [InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")] + [InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")] + [InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")] [InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")] [InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")] [InlineData("Superman: Red Son [providera id=4]", "providera id", "4")] [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] + [InlineData("Superman: Red Son {tmdbid=3)", "tmdbid", "3")] + [InlineData("Superman: Red Son (tvdbid-6}", "tvdbid", "6")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")] + [InlineData("{tmdbid=618355}", "tmdbid", "618355")] + [InlineData("(tmdbid=618355)", "tmdbid", "618355")] [InlineData("[tmdbid-618355]", "tmdbid", "618355")] + [InlineData("{tmdbid-618355)", "tmdbid", null)] + [InlineData("[tmdbid-618355}", "tmdbid", null)] [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")] [InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")] [InlineData("tmdbid=618355]", "tmdbid", null)] @@ -36,6 +44,9 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)] [InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)] [InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")] + [InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)] + [InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)] + [InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")] public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult) { Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute)); From ef73ed6ef7fd6bf2925d1ff503adaa4091d4a766 Mon Sep 17 00:00:00 2001 From: stevenaw <sweerdenburg@gmail.com> Date: Thu, 11 Dec 2025 22:15:19 -0500 Subject: [PATCH 062/390] optimize GetUniqueFlags() --- src/Jellyfin.Extensions/EnumerableExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs index 3eb9da01f2..0c78756236 100644 --- a/src/Jellyfin.Extensions/EnumerableExtensions.cs +++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs @@ -64,13 +64,13 @@ public static class EnumerableExtensions /// <typeparam name="T">The type of item.</typeparam> /// <returns>The IEnumerable{Enum}.</returns> public static IEnumerable<T> GetUniqueFlags<T>(this T flags) - where T : Enum + where T : struct, Enum { - foreach (Enum value in Enum.GetValues(flags.GetType())) + foreach (T value in Enum.GetValues<T>()) { if (flags.HasFlag(value)) { - yield return (T)value; + yield return value; } } } From c8bdee26b7485be7f251142fcf6becf33fcfcea2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:13:35 +0000 Subject: [PATCH 063/390] Update github/codeql-action action to v4.31.8 --- .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 5a6cccda0d..0823cf9be9 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 From 6b6d54a07cd9b5d1619a266a8cae8d20ef926cf7 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <nielsvanvelzen@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:26:22 +0100 Subject: [PATCH 064/390] Remove legacy API route middleware (#15669) --- .../LegacyEmbyRouteRewriteMiddleware.cs | 53 ------------------- .../ApiApplicationBuilderExtensions.cs | 13 ----- Jellyfin.Server/Startup.cs | 3 -- 3 files changed, 69 deletions(-) delete mode 100644 Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs diff --git a/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs b/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs deleted file mode 100644 index 2cbb183263..0000000000 --- a/Jellyfin.Api/Middleware/LegacyEmbyRouteRewriteMiddleware.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Api.Middleware; - -/// <summary> -/// Removes /emby and /mediabrowser from requested route. -/// </summary> -public class LegacyEmbyRouteRewriteMiddleware -{ - private const string EmbyPath = "/emby"; - private const string MediabrowserPath = "/mediabrowser"; - - private readonly RequestDelegate _next; - private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class. - /// </summary> - /// <param name="next">The next delegate in the pipeline.</param> - /// <param name="logger">The logger.</param> - public LegacyEmbyRouteRewriteMiddleware( - RequestDelegate next, - ILogger<LegacyEmbyRouteRewriteMiddleware> logger) - { - _next = next; - _logger = logger; - } - - /// <summary> - /// Executes the middleware action. - /// </summary> - /// <param name="httpContext">The current HTTP context.</param> - /// <returns>The async task.</returns> - public async Task Invoke(HttpContext httpContext) - { - var localPath = httpContext.Request.Path.ToString(); - if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase)) - { - httpContext.Request.Path = localPath[EmbyPath.Length..]; - _logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath); - } - else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase)) - { - httpContext.Request.Path = localPath[MediabrowserPath.Length..]; - _logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath); - } - - await _next(httpContext).ConfigureAwait(false); - } -} diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index a56baba33b..9fd853cf2e 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -117,18 +117,5 @@ namespace Jellyfin.Server.Extensions { return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>(); } - - /// <summary> - /// Adds /emby and /mediabrowser route trimming to the application pipeline. - /// </summary> - /// <remarks> - /// This must be injected before any path related middleware. - /// </remarks> - /// <param name="appBuilder">The application builder.</param> - /// <returns>The updated application builder.</returns> - public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder) - { - return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>(); - } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 5032b2aec1..f6a4ae7d6e 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -173,9 +173,6 @@ namespace Jellyfin.Server mainApp.UseHttpsRedirection(); } - // This must be injected before any path related middleware. - mainApp.UsePathTrim(); - if (appConfig.HostWebClient()) { var extensionProvider = new FileExtensionContentTypeProvider(); From dd480f96cdd341c8a986eb4cb7133eff3d535f63 Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:29:28 +0900 Subject: [PATCH 065/390] parse more information from book filenames (#15655) --- Emby.Naming/Book/BookFileNameParser.cs | 75 +++++++++++++++++++ Emby.Naming/Book/BookFileNameParserResult.cs | 41 ++++++++++ .../Library/Resolvers/Books/BookResolver.cs | 34 ++++++--- 3 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 Emby.Naming/Book/BookFileNameParser.cs create mode 100644 Emby.Naming/Book/BookFileNameParserResult.cs diff --git a/Emby.Naming/Book/BookFileNameParser.cs b/Emby.Naming/Book/BookFileNameParser.cs new file mode 100644 index 0000000000..28625f16de --- /dev/null +++ b/Emby.Naming/Book/BookFileNameParser.cs @@ -0,0 +1,75 @@ +using System.Text.RegularExpressions; + +namespace Emby.Naming.Book +{ + /// <summary> + /// Helper class to retrieve basic metadata from a book filename. + /// </summary> + public static class BookFileNameParser + { + private const string NameMatchGroup = "name"; + private const string IndexMatchGroup = "index"; + private const string YearMatchGroup = "year"; + private const string SeriesNameMatchGroup = "seriesName"; + + private static readonly Regex[] _nameMatches = + [ + // seriesName (seriesYear) #index (of count) (year) where only seriesName and index are required + new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>[0-9]{4})\))?)\s#(?<index>[0-9]+)((\s\(of\s(?<count>[0-9]+)\))?)((\s\((?<year>[0-9]{4})\))?)$"), + new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>[0-9]+)\)((\s\((?<year>[0-9]{4})\))?)$"), + new Regex(@"^(?<index>[0-9]+)\s\-\s(?<name>.+?)((\s\((?<year>[0-9]{4})\))?)$"), + new Regex(@"(?<name>.*)\((?<year>[0-9]{4})\)"), + // last resort matches the whole string as the name + new Regex(@"(?<name>.*)") + ]; + + /// <summary> + /// Parse a filename name to retrieve the book name, series name, index, and year. + /// </summary> + /// <param name="name">Book filename to parse for information.</param> + /// <returns>Returns <see cref="BookFileNameParserResult"/> object.</returns> + public static BookFileNameParserResult Parse(string? name) + { + var result = new BookFileNameParserResult(); + + if (name == null) + { + return result; + } + + foreach (var regex in _nameMatches) + { + var match = regex.Match(name); + + if (!match.Success) + { + continue; + } + + if (match.Groups.TryGetValue(NameMatchGroup, out Group? nameGroup) && nameGroup.Success) + { + result.Name = nameGroup.Value.Trim(); + } + + if (match.Groups.TryGetValue(IndexMatchGroup, out Group? indexGroup) && indexGroup.Success && int.TryParse(indexGroup.Value, out var index)) + { + result.Index = index; + } + + if (match.Groups.TryGetValue(YearMatchGroup, out Group? yearGroup) && yearGroup.Success && int.TryParse(yearGroup.Value, out var year)) + { + result.Year = year; + } + + if (match.Groups.TryGetValue(SeriesNameMatchGroup, out Group? seriesGroup) && seriesGroup.Success) + { + result.SeriesName = seriesGroup.Value.Trim(); + } + + break; + } + + return result; + } + } +} diff --git a/Emby.Naming/Book/BookFileNameParserResult.cs b/Emby.Naming/Book/BookFileNameParserResult.cs new file mode 100644 index 0000000000..f29716b9e3 --- /dev/null +++ b/Emby.Naming/Book/BookFileNameParserResult.cs @@ -0,0 +1,41 @@ +using System; + +namespace Emby.Naming.Book +{ + /// <summary> + /// Data object used to pass metadata parsed from a book filename. + /// </summary> + public class BookFileNameParserResult + { + /// <summary> + /// Initializes a new instance of the <see cref="BookFileNameParserResult"/> class. + /// </summary> + public BookFileNameParserResult() + { + Name = null; + Index = null; + Year = null; + SeriesName = null; + } + + /// <summary> + /// Gets or sets the name of the book. + /// </summary> + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the book index. + /// </summary> + public int? Index { get; set; } + + /// <summary> + /// Gets or sets the publication year. + /// </summary> + public int? Year { get; set; } + + /// <summary> + /// Gets or sets the series name. + /// </summary> + public string? SeriesName { get; set; } + } +} diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 464a548ab9..1e885aad6e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -5,12 +5,12 @@ using System; using System.IO; using System.Linq; +using Emby.Naming.Book; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Entities; namespace Emby.Server.Implementations.Library.Resolvers.Books { @@ -35,17 +35,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var extension = Path.GetExtension(args.Path.AsSpan()); - if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { - // It's a book - return new Book - { - Path = args.Path, - IsInMixedFolder = true - }; + return null; } - return null; + var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path)); + + return new Book + { + Path = args.Path, + Name = result.Name ?? string.Empty, + IndexNumber = result.Index, + ProductionYear = result.Year, + SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)), + IsInMixedFolder = true, + }; } private Book GetBook(ItemResolveArgs args) @@ -59,15 +64,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books StringComparison.OrdinalIgnoreCase); }).ToList(); - // Don't return a Book if there is more (or less) than one document in the directory + // directory is only considered a book when it contains exactly one supported file + // other library structures with multiple books to a directory will get picked up as individual files if (bookFiles.Count != 1) { return null; } + var result = BookFileNameParser.Parse(Path.GetFileName(args.Path)); + return new Book { - Path = bookFiles[0].FullName + Path = bookFiles[0].FullName, + Name = result.Name ?? string.Empty, + IndexNumber = result.Index, + ProductionYear = result.Year, + SeriesName = result.SeriesName ?? string.Empty, }; } } From 4db0ab0f40981b0775a3f6b5be020f38a93fb16f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 08:42:57 -0700 Subject: [PATCH 066/390] Update GitHub Artifact Actions (#15783) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-compat.yml | 8 ++++---- .github/workflows/ci-openapi.yml | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 8a755a3172..a3c49969ce 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 0a391dbe1b..46af68e587 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -27,7 +27,7 @@ jobs: - 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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: openapi-head retention-days: 14 @@ -61,7 +61,7 @@ jobs: - 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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: openapi-base retention-days: 14 @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-base path: openapi-base @@ -158,7 +158,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-head path: openapi-head From 771b0a7eabc7c3082dd5b328f121417385a1fc99 Mon Sep 17 00:00:00 2001 From: Luigi311 <git@luigi311.com> Date: Sat, 13 Dec 2025 08:43:49 -0700 Subject: [PATCH 067/390] Library: Async the SaveImages function (#15718) --- .../Library/LibraryManager.cs | 2 +- .../Item/BaseItemRepository.cs | 32 +++++++++++++------ .../Persistence/IItemRepository.cs | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cab87e53de..30c3e89b49 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2143,7 +2143,7 @@ namespace Emby.Server.Implementations.Library item.ValidateImages(); - _itemRepository.SaveImages(item); + await _itemRepository.SaveImagesAsync(item).ConfigureAwait(false); RegisterItem(item); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index dfe46ef8f6..9851d53c40 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -547,22 +547,34 @@ public sealed class BaseItemRepository } /// <inheritdoc /> - public void SaveImages(BaseItemDto item) + public async Task SaveImagesAsync(BaseItemDto item, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(item); - var images = item.ImageInfos.Select(e => Map(item.Id, e)); - using var context = _dbProvider.CreateDbContext(); + var images = item.ImageInfos.Select(e => Map(item.Id, e)).ToArray(); - if (!context.BaseItems.Any(bi => bi.Id == item.Id)) + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) { - _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); - return; - } + if (!await context.BaseItems + .AnyAsync(bi => bi.Id == item.Id, cancellationToken) + .ConfigureAwait(false)) + { + _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem"); + return; + } - context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); - context.BaseItemImageInfos.AddRange(images); - context.SaveChanges(); + await context.BaseItemImageInfos + .Where(e => e.ItemId == item.Id) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + + await context.BaseItemImageInfos + .AddRangeAsync(images, cancellationToken) + .ConfigureAwait(false); + + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } } /// <inheritdoc /> diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0026ab2b5f..00c492742a 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -33,7 +33,7 @@ public interface IItemRepository /// <param name="cancellationToken">The cancellation token.</param> void SaveItems(IReadOnlyList<BaseItem> items, CancellationToken cancellationToken); - void SaveImages(BaseItem item); + Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); /// <summary> /// Retrieves the item. From c4e8180b3c591b09bd76e7f49c620fc7cde7442b Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Mon, 15 Dec 2025 09:20:41 -0500 Subject: [PATCH 068/390] Update issue template version to 10.11.5 --- .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 ea3cd389e0..a505d4168f 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.5 - 10.11.4 - 10.11.3 - 10.11.2 From d0950c8f090ca56ec0a1874a0f8fb9195015582d Mon Sep 17 00:00:00 2001 From: SmeagolWorms4 <smeagolworms4@gmail.com> Date: Mon, 15 Dec 2025 13:47:14 +0100 Subject: [PATCH 069/390] Fix GET MetadataEditor ContentTypeOptions for inheritedContentType --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index e1d9b6bba0..28ea2033d8 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -180,11 +180,14 @@ public class ItemUpdateController : BaseJellyfinApiController info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentType = configuredContentType; - if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows) + if (inheritedContentType is null + || inheritedContentType == CollectionType.tvshows + || inheritedContentType == CollectionType.movies) { info.ContentTypeOptions = info.ContentTypeOptions .Where(i => string.IsNullOrWhiteSpace(i.Value) - || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase)) + || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase) + || string.Equals(i.Value, "Movies", StringComparison.OrdinalIgnoreCase)) .ToArray(); } } From 12a2f7c1a59d6024db36d01f96db683377af222f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= <jp_moura99@outlook.com> Date: Tue, 16 Dec 2025 10:23:15 -0500 Subject: [PATCH 070/390] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index c3eba362d2..17284854f6 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -125,8 +125,8 @@ "TaskKeyframeExtractor": "Extrator de Quadros-chave", "External": "Externo", "HearingImpaired": "Surdo", - "TaskRefreshTrickplayImages": "Gerar imagens de truques", - "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas.", + "TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay", + "TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", From 45a49a4fb4a559512e81d8f4577df40b87627484 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:10:15 +0000 Subject: [PATCH 071/390] Update github/codeql-action action to v4.31.9 --- .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 0823cf9be9..5ca3edd7ba 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 + uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 From 59d574edb7aca3a7a6ffdd139cc56111d7804ffc Mon Sep 17 00:00:00 2001 From: Michael <accounts@michi.onl> Date: Wed, 17 Dec 2025 17:11:14 -0500 Subject: [PATCH 072/390] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- .../Localization/Core/de.json | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index e60d03e468..8d86b2da13 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -9,9 +9,9 @@ "Channels": "Kanäle", "ChapterNameValue": "Kapitel {0}", "Collections": "Sammlungen", - "DeviceOfflineWithName": "{0} hat die Verbindung getrennt", - "DeviceOnlineWithName": "{0} ist verbunden", - "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", + "DeviceOfflineWithName": "{0} ist offline", + "DeviceOnlineWithName": "{0} ist online", + "FailedLoginAttemptWithUserName": "Anmeldung von {0} fehlgeschlagen", "Favorites": "Favoriten", "Folders": "Verzeichnisse", "Genres": "Genres", @@ -21,7 +21,7 @@ "HeaderFavoriteArtists": "Lieblingsinterpreten", "HeaderFavoriteEpisodes": "Lieblingsepisoden", "HeaderFavoriteShows": "Lieblingsserien", - "HeaderFavoriteSongs": "Lieblingslieder", + "HeaderFavoriteSongs": "Lieblingssongs", "HeaderLiveTV": "Live TV", "HeaderNextUp": "Als Nächstes", "HeaderRecordingGroups": "Aufnahme-Gruppen", @@ -46,7 +46,7 @@ "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.", "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar", "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert", - "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet", + "NotificationOptionAudioPlayback": "Audio wird abgespielt", "NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt", "NotificationOptionCameraImageUploaded": "Foto hochgeladen", "NotificationOptionInstallationFailed": "Installation fehlgeschlagen", @@ -57,11 +57,11 @@ "NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert", "NotificationOptionServerRestartRequired": "Serverneustart notwendig", "NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen", - "NotificationOptionUserLockedOut": "Benutzer ausgeschlossen", - "NotificationOptionVideoPlayback": "Videowiedergabe gestartet", + "NotificationOptionUserLockedOut": "Benutzer gesperrt", + "NotificationOptionVideoPlayback": "Video wird abgespielt", "NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt", "Photos": "Fotos", - "Playlists": "Wiedergabelisten", + "Playlists": "Playlists", "Plugin": "Plugin", "PluginInstalledWithName": "{0} wurde installiert", "PluginUninstalledWithName": "{0} wurde deinstalliert", @@ -82,7 +82,7 @@ "UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserDeletedWithName": "Benutzer {0} wurde gelöscht", "UserDownloadingItemWithValues": "{0} lädt {1} herunter", - "UserLockedOutWithName": "Benutzer {0} wurde ausgeschlossen", + "UserLockedOutWithName": "Benutzer {0} wurde gesperrt", "UserOfflineFromDevice": "{0} wurde getrennt von {1}", "UserOnlineFromDevice": "{0} ist online von {1}", "UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert", @@ -96,26 +96,26 @@ "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen", "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", "TaskRefreshChannels": "Kanäle aktualisieren", - "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", - "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen", + "TaskCleanTranscodeDescription": "Löscht temporäre Videodateien, die älter als 24 Stunden sind.", + "TaskCleanTranscode": "Temporäre Videodateien löschen", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", "TaskUpdatePlugins": "Plugins aktualisieren", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", "TaskRefreshPeople": "Personen aktualisieren", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", - "TaskCleanLogs": "Log-Verzeichnis aufräumen", - "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.", + "TaskCleanLogs": "Alte Protokolle löschen", + "TaskRefreshLibraryDescription": "Sucht nach neuen Dateien und aktualisiert Infos zu deinen Medien.", "TaskRefreshLibrary": "Medien-Bibliothek scannen", - "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.", - "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren", - "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.", - "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen", + "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videokapitel.", + "TaskRefreshChapterImages": "Kapitelvorschauen erstellen", + "TaskCleanCacheDescription": "Entfernt nicht mehr benötigte Cache-Dateien.", + "TaskCleanCache": "Cache leeren", "TasksChannelsCategory": "Internet-Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", "TasksMaintenanceCategory": "Wartung", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", - "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen", + "TaskCleanActivityLog": "Aktivitätsverlauf bereinigen", "Undefined": "Undefiniert", "Forced": "Erzwungen", "Default": "Standard", From 2cca942ce608bc9c56db110d6684a3b127b9c9c2 Mon Sep 17 00:00:00 2001 From: Kirill Nikiforov <me@allmazz.me> Date: Thu, 18 Dec 2025 22:56:33 +0100 Subject: [PATCH 073/390] Add myself to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0a4114478f..646cc15cd9 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -205,6 +205,7 @@ - [theshoeshiner](https://github.com/theshoeshiner) - [TokerX](https://github.com/TokerX) - [GeneMarks](https://github.com/GeneMarks) + - [Kirill Nikiforov](https://github.com/allmazz) # Emby Contributors From ee676fd568961e9a363cd3ed4f33d307dca7b20e Mon Sep 17 00:00:00 2001 From: Fabrizio Mansilla <fabrizio.mansilla.2005@gmail.com> Date: Thu, 18 Dec 2025 23:07:35 -0500 Subject: [PATCH 074/390] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- Emby.Server.Implementations/Localization/Core/es-AR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 012f793a6c..1f8af4c8a5 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskStartedWithName": "{0} iniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", - "Shows": "Programas", + "Shows": "Series", "Songs": "Canciones", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", From d446fde0091b3b101b2735f21dd24e49f0fd5660 Mon Sep 17 00:00:00 2001 From: Kityn <kitynska@gmail.com> Date: Thu, 18 Dec 2025 01:43:23 -0500 Subject: [PATCH 075/390] Translated using Weblate (Polish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pl/ --- Emby.Server.Implementations/Localization/Core/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index 3555ea4ae8..8ca22ac046 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -125,8 +125,8 @@ "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.", "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych", "HearingImpaired": "Niedosłyszący", - "TaskRefreshTrickplayImages": "Generuj obrazy trickplay", - "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach.", + "TaskRefreshTrickplayImages": "Generuj obrazy Trickplay", + "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy Trickplay dla filmów we włączonych bibliotekach.", "TaskCleanCollectionsAndPlaylistsDescription": "Usuwa elementy z kolekcji i list odtwarzania, które już nie istnieją.", "TaskCleanCollectionsAndPlaylists": "Oczyść kolekcje i listy odtwarzania", "TaskAudioNormalization": "Normalizacja dźwięku", From 84f66dd54e74621e4d81cd57648c4d27411d82d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Tenje=20Persson?= <bjorn@tenje.se> Date: Sat, 20 Dec 2025 04:36:38 +0100 Subject: [PATCH 076/390] Fixed Multi Sort in New ActivityManager (#15820) --- CONTRIBUTORS.md | 1 + .../Controllers/ActivityLogController.cs | 3 +++ Jellyfin.Data/Queries/ActivityLogQuery.cs | 5 +++++ .../Activity/ActivityManager.cs | 21 ++++++++++++++++--- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0a4114478f..4406413daa 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -205,6 +205,7 @@ - [theshoeshiner](https://github.com/theshoeshiner) - [TokerX](https://github.com/TokerX) - [GeneMarks](https://github.com/GeneMarks) + - [bjorntp](https://github.com/bjorntp) # Emby Contributors diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index d5f2627739..47d3f4b7f7 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -38,6 +38,7 @@ public class ActivityLogController : BaseJellyfinApiController /// <param name="startIndex">The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="limit">The maximum number of records to return.</param> /// <param name="minDate">The minimum date.</param> + /// <param name="maxDate">The maximum date.</param> /// <param name="hasUserId">Filter log entries if it has user id, or not.</param> /// <param name="name">Filter by name.</param> /// <param name="overview">Filter by overview.</param> @@ -56,6 +57,7 @@ public class ActivityLogController : BaseJellyfinApiController [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] DateTime? minDate, + [FromQuery] DateTime? maxDate, [FromQuery] bool? hasUserId, [FromQuery] string? name, [FromQuery] string? overview, @@ -72,6 +74,7 @@ public class ActivityLogController : BaseJellyfinApiController Skip = startIndex, Limit = limit, MinDate = minDate, + MaxDate = maxDate, HasUserId = hasUserId, Name = name, Overview = overview, diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs index 95c52f8705..6de6c4c217 100644 --- a/Jellyfin.Data/Queries/ActivityLogQuery.cs +++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs @@ -21,6 +21,11 @@ public class ActivityLogQuery : PaginatedQuery /// </summary> public DateTime? MinDate { get; set; } + /// <summary> + /// Gets or sets the maximum date to query for. + /// </summary> + public DateTime? MaxDate { get; set; } + /// <summary> /// Gets or sets the name filter. /// </summary> diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 7ee573f538..fe987b9d86 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -72,6 +72,11 @@ public class ActivityManager : IActivityManager entries = entries.Where(e => e.ActivityLog.DateCreated >= query.MinDate.Value); } + if (query.MaxDate is not null) + { + entries = entries.Where(e => e.ActivityLog.DateCreated <= query.MaxDate.Value); + } + if (!string.IsNullOrEmpty(query.Name)) { entries = entries.Where(e => EF.Functions.Like(e.ActivityLog.Name, $"%{query.Name}%")); @@ -166,9 +171,19 @@ public class ActivityManager : IActivityManager foreach (var (sortBy, sortOrder) in sorting) { var orderBy = MapOrderBy(sortBy); - ordered = sortOrder == SortOrder.Ascending - ? (ordered ?? query).OrderBy(orderBy) - : (ordered ?? query).OrderByDescending(orderBy); + + if (ordered == null) + { + ordered = sortOrder == SortOrder.Ascending + ? query.OrderBy(orderBy) + : query.OrderByDescending(orderBy); + } + else + { + ordered = sortOrder == SortOrder.Ascending + ? ordered.ThenBy(orderBy) + : ordered.ThenByDescending(orderBy); + } } return ordered; From 93902fc610a9d8b52780d88f7bb986e668567c9d Mon Sep 17 00:00:00 2001 From: john janzen <git@johnjanzen.me> Date: Sat, 20 Dec 2025 19:42:51 +0100 Subject: [PATCH 077/390] 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<IPData>(); if (readIpv4 && readIpv6) From fa99b1d81c5e8802e0aaecf4718c6c1413f2a1ac Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Sun, 5 Oct 2025 12:53:17 +0200 Subject: [PATCH 078/390] fix: remote subtitles --- .../Subtitles/SubtitleEncoder.cs | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 49ac0fa033..095d65a542 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -172,24 +172,26 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken) { - if (fileInfo.IsExternal) + if (fileInfo.Protocol == MediaProtocol.Http) { - var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + var result = await DetectCharset(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false); + var detected = result.Detected; + + if (detected is not null) { - var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); - var detected = result.Detected; - stream.Position = 0; + _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path); - if (detected is not null) - { - _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(new Uri(fileInfo.Path), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); - using var reader = new StreamReader(stream, detected.Encoding); - var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); - return new MemoryStream(Encoding.UTF8.GetBytes(text)); - } + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(stream, detected.Encoding); + var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } @@ -941,42 +943,46 @@ namespace MediaBrowser.MediaEncoding.Subtitles .ConfigureAwait(false); } - var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); + var charset = result.Detected?.EncodingName ?? string.Empty; + + // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding + if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) + && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase) + || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase))) { - var result = await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); - var charset = result.Detected?.EncodingName ?? string.Empty; - - // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding - if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) - && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase) - || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase))) - { - charset = string.Empty; - } - - _logger.LogDebug("charset {0} detected for {Path}", charset, path); - - return charset; + charset = string.Empty; } + + _logger.LogDebug("charset {0} detected for {Path}", charset, path); + + return charset; } - private async Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private async Task<DetectionResult> DetectCharset(string path, MediaProtocol protocol, CancellationToken cancellationToken) { switch (protocol) { case MediaProtocol.Http: - { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(new Uri(path), cancellationToken) - .ConfigureAwait(false); - return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - } + { + using var resp = await _httpClientFactory + .CreateClient(NamedClient.Default) + .GetAsync(new Uri(path), HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + resp.EnsureSuccessStatusCode(); + + using var s = await resp.Content.ReadAsStreamAsync(cancellationToken); + return await CharsetDetector.DetectFromStreamAsync(s, cancellationToken); + } case MediaProtocol.File: - return AsyncFile.OpenRead(path); + { + return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) + .ConfigureAwait(false); + } + default: - throw new ArgumentOutOfRangeException(nameof(protocol)); + throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol"); } } From ddc613cd726604b8b881c4ea99485d7e7e3d6c3f Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Sun, 5 Oct 2025 16:06:59 +0200 Subject: [PATCH 079/390] fix CA2007 --- .../Subtitles/SubtitleEncoder.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 095d65a542..63067cdb47 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -187,11 +187,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var reader = new StreamReader(stream, detected.Encoding); - var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + using var reader = new StreamReader(stream, detected.Encoding); + var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return new MemoryStream(Encoding.UTF8.GetBytes(text)); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } } } @@ -967,12 +970,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using var resp = await _httpClientFactory .CreateClient(NamedClient.Default) - .GetAsync(new Uri(path), HttpCompletionOption.ResponseHeadersRead, cancellationToken); + .GetAsync(new Uri(path), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); resp.EnsureSuccessStatusCode(); - using var s = await resp.Content.ReadAsStreamAsync(cancellationToken); - return await CharsetDetector.DetectFromStreamAsync(s, cancellationToken); + using var s = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return await CharsetDetector.DetectFromStreamAsync(s, cancellationToken).ConfigureAwait(false); } case MediaProtocol.File: From e5a2acd6dd901d4d9c57e80051c1b096efcf0f94 Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Sun, 5 Oct 2025 18:01:00 +0200 Subject: [PATCH 080/390] ise codec before path on format selection --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 63067cdb47..7fde71575b 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -223,7 +223,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles }; } - var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) + var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) .TrimStart('.'); // Handle PGS subtitles as raw streams for the client to render From 2168847a45721c567c69767be28c4ce51fe5523a Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Sat, 29 Nov 2025 10:11:18 +0100 Subject: [PATCH 081/390] wip --- MediaBrowser.Model/Dlna/StreamInfo.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 92404de508..9cbdf03e17 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1260,11 +1260,10 @@ public class StreamInfo stream.Index.ToString(CultureInfo.InvariantCulture), startPositionTicks.ToString(CultureInfo.InvariantCulture), subtitleProfile.Format); - info.IsExternalUrl = false; // Default to API URL + info.IsExternalUrl = false; // Check conditions for potentially using the direct path if (stream.IsExternal // Must be external - && MediaSource?.Protocol != MediaProtocol.File // Main media must not be a local file && string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) // Format must match (no conversion needed) && !string.IsNullOrEmpty(stream.Path) // Path must exist && Uri.TryCreate(stream.Path, UriKind.Absolute, out Uri? uriResult) // Path must be an absolute URI From 172b054f487c185efbe3f83639e1e896ca38dcb3 Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Sat, 29 Nov 2025 10:20:18 +0100 Subject: [PATCH 082/390] wip --- MediaBrowser.Model/Dlna/StreamInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 9cbdf03e17..3efb143bc3 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1264,6 +1264,7 @@ public class StreamInfo // Check conditions for potentially using the direct path if (stream.IsExternal // Must be external + && stream.SupportsExternalStream && string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) // Format must match (no conversion needed) && !string.IsNullOrEmpty(stream.Path) // Path must exist && Uri.TryCreate(stream.Path, UriKind.Absolute, out Uri? uriResult) // Path must be an absolute URI From 8d8d38600ec542f32060a62f697fa944393edc48 Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Mon, 1 Dec 2025 10:24:09 +0100 Subject: [PATCH 083/390] wip --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 7fde71575b..6b1c5f952f 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -181,13 +181,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(new Uri(fileInfo.Path), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + using var stream = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetStreamAsync(new Uri(fileInfo.Path), cancellationToken) .ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false)) { using var reader = new StreamReader(stream, detected.Encoding); From 7f1a0ff6fce22d4e2f3919e92548d43cdc0229c9 Mon Sep 17 00:00:00 2001 From: lostb1t <coding-mosses0z@icloud.com> Date: Mon, 1 Dec 2025 12:00:08 +0100 Subject: [PATCH 084/390] wip --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 6b1c5f952f..bf7ec05a96 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -965,15 +965,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { case MediaProtocol.Http: { - using var resp = await _httpClientFactory + using var stream = await _httpClientFactory .CreateClient(NamedClient.Default) - .GetAsync(new Uri(path), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .GetStreamAsync(new Uri(path), cancellationToken) .ConfigureAwait(false); - resp.EnsureSuccessStatusCode(); - - using var s = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await CharsetDetector.DetectFromStreamAsync(s, cancellationToken).ConfigureAwait(false); + return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); } case MediaProtocol.File: From 146681f0ba927b6c2d1e392a2b157a28c36e1a6b Mon Sep 17 00:00:00 2001 From: john janzen <git@johnjanzen.me> Date: Sun, 21 Dec 2025 15:37:22 +0100 Subject: [PATCH 085/390] 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<SetupServer>(), 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<NetworkManager>(), 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 /// <inheritdoc/> public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) { - return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); + return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); } /// <summary> /// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound. /// </summary> + /// <param name="logger">Logger to use for messages.</param> /// <param name="individualInterfaces">Defines that only known interfaces should be used.</param> /// <param name="configurationManager">The ConfigurationManager.</param> /// <param name="knownInterfaces">The known interfaces that gets returned if possible or instructed.</param> @@ -766,6 +767,7 @@ public class NetworkManager : INetworkManager, IDisposable /// <param name="readIpv6">Include IPV6 type interfaces.</param> /// <returns>A list of ip address of which jellyfin should bind to.</returns> public static IReadOnlyList<IPData> GetAllBindInterfaces( + ILogger<NetworkManager> logger, bool individualInterfaces, IConfigurationManager configurationManager, IReadOnlyList<IPData> 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<IPData>(); From 0e73a56a457cd5b91673da2e42304066aadffbdb Mon Sep 17 00:00:00 2001 From: Translation expert <apk2000@users.noreply.translate.jellyfin.org> Date: Thu, 25 Dec 2025 12:20:18 -0500 Subject: [PATCH 086/390] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 24ed116f39..d09a7884e7 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -2,13 +2,13 @@ "Albums": "ألبومات", "AppDeviceValues": "تطبيق: {0}, جهاز: {1}", "Application": "تطبيق", - "Artists": "الفنانون", + "Artists": "فنانون", "AuthenticationSucceededWithUserName": "نجحت عملية التوثيق بـ {0}", "Books": "الكتب", "CameraImageUploadedFrom": "رُفعت صورة الكاميرا الجديدة من {0}", "Channels": "القنوات", "ChapterNameValue": "الفصل {0}", - "Collections": "المجموعات", + "Collections": "مجموعات", "DeviceOfflineWithName": "قُطِع الاتصال ب{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "محاولة تسجيل الدخول فاشلة من {0}", @@ -16,7 +16,7 @@ "Folders": "المجلدات", "Genres": "التصنيفات", "HeaderAlbumArtists": "فناني الألبوم", - "HeaderContinueWatching": "أكمل المشاهدة", + "HeaderContinueWatching": "متابعة المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", "HeaderFavoriteEpisodes": "الحلقات المفضلة", From 7bafd135647b0de029cb5d980e294b9256109590 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:48:03 +0000 Subject: [PATCH 087/390] Update dependency Xunit.SkippableFact to 1.5.61 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 564223483d..14ff3fadbe 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -93,7 +93,7 @@ <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> - <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" /> + <PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" /> <PackageVersion Include="xunit" Version="2.9.3" /> </ItemGroup> </Project> From c30654c33c0561ee7f3b7d096038f9604a111003 Mon Sep 17 00:00:00 2001 From: MrPlow <joachim.huffer@gmail.com> Date: Sat, 27 Dec 2025 02:16:02 -0500 Subject: [PATCH 088/390] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- .../Localization/Core/de.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 8d86b2da13..278d78ae44 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -96,20 +96,20 @@ "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen", "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", "TaskRefreshChannels": "Kanäle aktualisieren", - "TaskCleanTranscodeDescription": "Löscht temporäre Videodateien, die älter als 24 Stunden sind.", - "TaskCleanTranscode": "Temporäre Videodateien löschen", + "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", + "TaskCleanTranscode": "Transkodierungsverzeichnis leeren", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", "TaskUpdatePlugins": "Plugins aktualisieren", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", "TaskRefreshPeople": "Personen aktualisieren", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", - "TaskCleanLogs": "Alte Protokolle löschen", - "TaskRefreshLibraryDescription": "Sucht nach neuen Dateien und aktualisiert Infos zu deinen Medien.", + "TaskCleanLogs": "Protokollverzeichnis leeren", + "TaskRefreshLibraryDescription": "Durchsucht deine Medienbibliothek nach neuen Dateien und aktualisiert Metadaten.", "TaskRefreshLibrary": "Medien-Bibliothek scannen", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videokapitel.", "TaskRefreshChapterImages": "Kapitelvorschauen erstellen", - "TaskCleanCacheDescription": "Entfernt nicht mehr benötigte Cache-Dateien.", - "TaskCleanCache": "Cache leeren", + "TaskCleanCacheDescription": "Löscht Cache-Dateien, die vom System nicht mehr benötigt werden.", + "TaskCleanCache": "Cache-Verzeichnis leeren", "TasksChannelsCategory": "Internet-Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", From 24acd94015d62673becd73a1f2ec4943433644ba Mon Sep 17 00:00:00 2001 From: liszto <liszto@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:11 -0500 Subject: [PATCH 089/390] Backport pull request #15659 from jellyfin/release-10.11.z Fix thumbnails never deletes from temp folder ( issue #15629 ) Original-merge: 636908fc4dc4cd69a1c20949a5f7c6cba25de67a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 86 ++++++++++++++++-------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 503e2f941f..c6eab92ead 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -209,39 +209,69 @@ public class SkiaEncoder : IImageEncoder return default; } - using var codec = SKCodec.Create(safePath, out var result); - - switch (result) + SKCodec? codec = null; + bool isSafePathTemp = !string.Equals(Path.GetFullPath(safePath), Path.GetFullPath(path), StringComparison.OrdinalIgnoreCase); + try { - case SKCodecResult.Success: - // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel - // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.) - // `SKCodec.Create` returns a *non‑null* codec together with - // SKCodecResult.InternalError. The header still contains valid dimensions, - // which is all we need here – so we fall back to them instead of aborting. - // See e.g. Skia bugs #4139, #6092. - case SKCodecResult.InternalError when codec is not null: - var info = codec.Info; - return new ImageDimensions(info.Width, info.Height); - - case SKCodecResult.Unimplemented: - _logger.LogDebug("Image format not supported: {FilePath}", path); - return default; - - default: + codec = SKCodec.Create(safePath, out var result); + switch (result) { - var boundsInfo = SKBitmap.DecodeBounds(safePath); + case SKCodecResult.Success: + // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel + // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.) + // `SKCodec.Create` returns a *non‑null* codec together with + // SKCodecResult.InternalError. The header still contains valid dimensions, + // which is all we need here – so we fall back to them instead of aborting. + // See e.g. Skia bugs #4139, #6092. + case SKCodecResult.InternalError when codec is not null: + var info = codec.Info; + return new ImageDimensions(info.Width, info.Height); - if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + case SKCodecResult.Unimplemented: + _logger.LogDebug("Image format not supported: {FilePath}", path); + return default; + + default: { - return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); - } + var boundsInfo = SKBitmap.DecodeBounds(safePath); + if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + { + return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + } - _logger.LogWarning( - "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", - path, - result); - return default; + _logger.LogWarning( + "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", + path, + result); + + return default; + } + } + } + finally + { + try + { + codec?.Dispose(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error by closing codec for {FilePath}", safePath); + } + + if (isSafePathTemp) + { + try + { + if (File.Exists(safePath)) + { + File.Delete(safePath); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Unable to remove temporary file '{TempPath}'", safePath); + } } } } From 8a0b963d2cf7294364bee1fa519c9a89d6e8ddcf Mon Sep 17 00:00:00 2001 From: SapientGuardian <SapientGuardian@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:12 -0500 Subject: [PATCH 090/390] Backport pull request #15662 from jellyfin/release-10.11.z Fix blocking in async context in LimitedConcurrencyLibraryScheduler Original-merge: d91adb5d54ed706198cd3066608107bbfeedebc1 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- CONTRIBUTORS.md | 1 + .../LimitedConcurrencyLibraryScheduler.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d475dbba76..43954c0837 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -117,6 +117,7 @@ - [sachk](https://github.com/sachk) - [sammyrc34](https://github.com/sammyrc34) - [samuel9554](https://github.com/samuel9554) + - [SapientGuardian](https://github.com/SapientGuardian) - [scheidleon](https://github.com/scheidleon) - [sebPomme](https://github.com/sebPomme) - [SegiH](https://github.com/SegiH) diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs index 0de5f198d7..5c805e9e49 100644 --- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs +++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using Microsoft.Extensions.Hosting; @@ -29,7 +30,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr /// </summary> private readonly Lock _taskLock = new(); - private readonly BlockingCollection<TaskQueueItem> _tasks = new(); + private readonly Channel<TaskQueueItem> _tasks = Channel.CreateUnbounded<TaskQueueItem>(); private volatile int _workCounter; private Task? _cleanupTask; @@ -77,7 +78,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr lock (_taskLock) { - if (_tasks.Count > 0 || _workCounter > 0) + if (_tasks.Reader.Count > 0 || _workCounter > 0) { _logger.LogDebug("Delay cleanup task, operations still running."); // tasks are still there so its still in use. Reschedule cleanup task. @@ -144,9 +145,9 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr _deadlockDetector.Value = stopToken.TaskStop; try { - foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token)) + while (!stopToken.GlobalStop.Token.IsCancellationRequested) { - stopToken.GlobalStop.Token.ThrowIfCancellationRequested(); + var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false); try { var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0; @@ -264,7 +265,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr for (var i = 0; i < workItems.Length; i++) { var item = workItems[i]!; - _tasks.Add(item, CancellationToken.None); + await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false); } if (_deadlockDetector.Value is not null) @@ -304,13 +305,12 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr } _disposed = true; - _tasks.CompleteAdding(); + _tasks.Writer.Complete(); foreach (var item in _taskRunners) { await item.Key.CancelAsync().ConfigureAwait(false); } - _tasks.Dispose(); if (_cleanupTask is not null) { await _cleanupTask.ConfigureAwait(false); From 8461268837b1a10fef8bb0c976b017da437bf24a Mon Sep 17 00:00:00 2001 From: andrewrabert <andrewrabert@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:13 -0500 Subject: [PATCH 091/390] Backport pull request #15666 from jellyfin/release-10.11.z Fix unnecessary database JOINs in ApplyNavigations Original-merge: 4cdd8c8233cc8e2b4ced9be5b7ddbd48f190a3b9 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 9851d53c40..6b060430e6 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -410,10 +410,25 @@ public sealed class BaseItemRepository private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter) { - dbQuery = dbQuery.Include(e => e.TrailerTypes) - .Include(e => e.Provider) - .Include(e => e.LockedFields) - .Include(e => e.UserData); + if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + { + dbQuery = dbQuery.Include(e => e.TrailerTypes); + } + + if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds)) + { + dbQuery = dbQuery.Include(e => e.Provider); + } + + if (filter.DtoOptions.ContainsField(ItemFields.Settings)) + { + dbQuery = dbQuery.Include(e => e.LockedFields); + } + + if (filter.DtoOptions.EnableUserData) + { + dbQuery = dbQuery.Include(e => e.UserData); + } if (filter.DtoOptions.EnableImages) { From 8e2ed40a8bf309c76a2e3b468d606392554cfe8b Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:14 -0500 Subject: [PATCH 092/390] Backport pull request #15680 from jellyfin/release-10.11.z Fix ItemAdded event triggering when updating metadata Original-merge: fb65f8f8532fbad22089a3a3cfb4d9237c71c567 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 43f0746ba7..f8e2aece1f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -721,8 +721,6 @@ namespace MediaBrowser.Providers.Manager } } } - - _libraryManager.CreateItem(item, null); } /// <summary> From 5d50ff5f81872cc9c2b1611c878162a39714a1a8 Mon Sep 17 00:00:00 2001 From: ivanjx <ivanjx@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:15 -0500 Subject: [PATCH 093/390] Backport pull request #15681 from jellyfin/release-10.11.z Fix symlinked file size Original-merge: d32f487e8e4762bba740b586285b663712eda69a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Controllers/LibraryController.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4e898c00e8..558e1c6c80 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -23,6 +23,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; @@ -700,7 +701,18 @@ public class LibraryController : BaseJellyfinApiController // Quotes are valid in linux. They'll possibly cause issues here. var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal); - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true); + var filePath = item.Path; + if (item.IsFileProtocol) + { + // PhysicalFile does not work well with symlinks at the moment. + var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true); + if (resolved is not null && resolved.Exists) + { + filePath = resolved.FullName; + } + } + + return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), filename, true); } /// <summary> From 55570043759bcfa3c76df7e94d3303257171c970 Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:17 -0500 Subject: [PATCH 094/390] Backport pull request #15689 from jellyfin/release-10.11.z Use original name for MusicAritist matching Original-merge: 4c5a3fbff34a603ff0344e0b42d07bc17f31f92c Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + .../Item/BaseItemRepository.cs | 11 +++++++++-- .../Entities/InternalItemsQuery.cs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 30c3e89b49..f35d85f659 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1058,6 +1058,7 @@ namespace Emby.Server.Implementations.Library { IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, + UseRawName = true, DtoOptions = options }).Cast<MusicArtist>() .OrderBy(i => i.IsAccessedByName ? 1 : 0) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 6b060430e6..98072918c4 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1987,8 +1987,15 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.Name)) { - var cleanName = GetCleanValue(filter.Name); - baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + if (filter.UseRawName == true) + { + baseQuery = baseQuery.Where(e => e.Name == filter.Name); + } + else + { + var cleanName = GetCleanValue(filter.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } } // These are the same, for now diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index b32b64f5da..076a592922 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities public string? Name { get; set; } + public bool? UseRawName { get; set; } + public string? Person { get; set; } public Guid[] PersonIds { get; set; } From 1af1c72e81c3100c5db1a58d853818910c125c10 Mon Sep 17 00:00:00 2001 From: martenumberto <martenumberto@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:18 -0500 Subject: [PATCH 095/390] Backport pull request #15690 from jellyfin/release-10.11.z Fix: Add .ts fallback for video streams to prevent crash Original-merge: 2a0b90e3852edae22d9f7cec197e6e81e9415632 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- CONTRIBUTORS.md | 1 + Jellyfin.Api/Helpers/StreamingHelpers.cs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 43954c0837..0fd509f842 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -208,6 +208,7 @@ - [GeneMarks](https://github.com/GeneMarks) - [Kirill Nikiforov](https://github.com/allmazz) - [bjorntp](https://github.com/bjorntp) + - [martenumberto](https://github.com/martenumberto) # Emby Contributors diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 2601fa3be8..b3f5b9a801 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -159,6 +159,13 @@ public static class StreamingHelpers string? containerInternal = Path.GetExtension(state.RequestedUrl); + if (string.IsNullOrEmpty(containerInternal) + && (!string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId) + || (mediaSource != null && mediaSource.IsInfiniteStream))) + { + containerInternal = ".ts"; + } + if (!string.IsNullOrEmpty(streamingRequest.Container)) { containerInternal = streamingRequest.Container; From 580585846b618bf308ebfd4e698ff0efe1a2de4d Mon Sep 17 00:00:00 2001 From: myzhysz <myzhysz@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:19 -0500 Subject: [PATCH 096/390] Backport pull request #15698 from jellyfin/release-10.11.z Fix stack overflow during scan (#15000) Original-merge: dde70fd8a2007f52f87546eb3c3acf8963333c4c Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../LimitedConcurrencyLibraryScheduler.cs | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs index 5c805e9e49..2811a081aa 100644 --- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs +++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs @@ -243,7 +243,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr }; }).ToArray(); - if (ShouldForceSequentialOperation()) + if (ShouldForceSequentialOperation() || _deadlockDetector.Value is not null) { _logger.LogDebug("Process sequentially."); try @@ -268,32 +268,11 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false); } - if (_deadlockDetector.Value is not null) - { - _logger.LogDebug("Nested invocation detected, process in-place."); - try - { - // we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved - while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token)) - { - await ProcessItem(item).ConfigureAwait(false); - } - } - catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested) - { - // operation is cancelled. Do nothing. - } - - _logger.LogDebug("process in-place done."); - } - else - { - Worker(); - _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length); - await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false); - _logger.LogDebug("{NoWorkers} completed.", workItems.Length); - ScheduleTaskCleanup(); - } + Worker(); + _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length); + await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false); + _logger.LogDebug("{NoWorkers} completed.", workItems.Length); + ScheduleTaskCleanup(); } /// <inheritdoc/> From b9cf26db2f3d219340a17951c289a230d0ccf31a Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:20 -0500 Subject: [PATCH 097/390] Backport pull request #15746 from jellyfin/release-10.11.z Skip invalid ignore rules Original-merge: 6e60634c9f078cc01e343b07a0a6b2a5c230478c Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Library/DotIgnoreIgnoreRule.cs | 48 +++++++++- .../Library/DotIgnoreIgnoreRuleTest.cs | 87 +++++++++++++++---- 2 files changed, 115 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 473ff8e1d7..ef5d24c70f 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -70,12 +71,55 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule { // If file has content, base ignoring off the content .gitignore-style rules var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return CheckIgnoreRules(path, rules, isDirectory); + } + + /// <summary> + /// Checks whether a path should be ignored based on an array of ignore rules. + /// </summary> + /// <param name="path">The path to check.</param> + /// <param name="rules">The array of ignore rules.</param> + /// <param name="isDirectory">Whether the path is a directory.</param> + /// <returns>True if the path should be ignored.</returns> + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory) + => CheckIgnoreRules(path, rules, isDirectory, IsWindows); + + /// <summary> + /// Checks whether a path should be ignored based on an array of ignore rules. + /// </summary> + /// <param name="path">The path to check.</param> + /// <param name="rules">The array of ignore rules.</param> + /// <param name="isDirectory">Whether the path is a directory.</param> + /// <param name="normalizePath">Whether to normalize backslashes to forward slashes (for Windows paths).</param> + /// <returns>True if the path should be ignored.</returns> + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath) + { var ignore = new Ignore.Ignore(); - ignore.Add(rules); + + // Add each rule individually to catch and skip invalid patterns + var validRulesAdded = 0; + foreach (var rule in rules) + { + try + { + ignore.Add(rule); + validRulesAdded++; + } + catch (RegexParseException) + { + // Ignore invalid patterns + } + } + + // If no valid rules were added, fall back to ignoring everything (like an empty .ignore file) + if (validRulesAdded == 0) + { + return true; + } // Mitigate the problem of the Ignore library not handling Windows paths correctly. // See https://github.com/jellyfin/jellyfin/issues/15484 - var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + var pathToCheck = normalizePath ? path.NormalizePath('/') : path; // Add trailing slash for directories to match "folder/" if (isDirectory) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index d677c9f091..a7bbef7ed4 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,30 +1,81 @@ +using Emby.Server.Implementations.Library; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; public class DotIgnoreIgnoreRuleTest { - [Fact] - public void Test() + private static readonly string[] _rule1 = ["SPs"]; + private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"]; + private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"]; + private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"]; + + public static TheoryData<string[], string, bool, bool> CheckIgnoreRulesTestData => + new() + { + // Basic pattern matching + { _rule1, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule1, "cd/sps/ffffff.mkv", false, true }, + { _rule1, "/cd/sps/ffffff.mkv", false, true }, + + // Negate pattern + { _rule2, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule2, "cd/sps/ffffff.mkv", false, true }, + { _rule2, "/cd/sps/ffffff.mkv", false, true }, + { _rule2, "f:/cd/sps/thebestshot.mkv", false, false }, + { _rule2, "cd/sps/thebestshot.mkv", false, false }, + { _rule2, "/cd/sps/thebestshot.mkv", false, false }, + + // Mixed valid and invalid patterns - skips invalid, applies valid + { _rule3, "test.txt", false, true }, + { _rule3, "videos/movie.mp4", false, true }, + { _rule3, "movie.mkv", false, true }, + { _rule3, "test.mp3", false, false }, + + // Only invalid patterns - falls back to ignore all + { _rule4, "any-file.txt", false, true }, + { _rule4, "any/path/to/file.mkv", false, true }, + }; + + public static TheoryData<string[], string, bool, bool> WindowsPathNormalizationTestData => + new() + { + // Windows paths with backslashes - should match when normalizePath is true + { _rule1, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule1, @"D:\media\sps\movie.mkv", false, true }, + { _rule1, @"\\server\share\sps\file.mkv", false, true }, + + // Negate pattern with Windows paths + { _rule2, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule2, @"C:\cd\sps\thebestshot.mkv", false, false }, + + // Directory matching with Windows paths + { _rule3, @"C:\videos\movie.mp4", false, true }, + { _rule3, @"D:\documents\test.txt", false, true }, + { _rule3, @"E:\music\song.mp3", false, false }, + }; + + [Theory] + [MemberData(nameof(CheckIgnoreRulesTestData))] + public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory)); } - [Fact] - public void TestNegatePattern() + [Theory] + [MemberData(nameof(WindowsPathNormalizationTestData))] + public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - ignore.Add("!thebestshot.mkv"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); - Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv")); + // With normalizePath=true, backslashes should be converted to forward slashes + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true)); + } + + [Theory] + [InlineData(@"C:\cd\sps\ffffff.mkv")] + [InlineData(@"D:\media\sps\movie.mkv")] + public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path) + { + // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes + Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } } From 2a464c316de16242012e2e45a13d1c7a5f709186 Mon Sep 17 00:00:00 2001 From: Collin-Swish <79892877+Collin-Swish@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:21 -0500 Subject: [PATCH 098/390] Backport pull request #15752 from jellyfin/release-10.11.z Fix case sensitivity edge case Original-merge: b50ce1ad6b3239245897a648dd24cf407138abfc Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 63d962ad97..501cb4fbe8 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -149,7 +149,7 @@ namespace Jellyfin.Server.Implementations.Users ThrowIfInvalidUsername(newName); - if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)) + if (user.Username.Equals(newName, StringComparison.Ordinal)) { throw new ArgumentException("The new and old names must be different."); } From f867ce38424e9f461ef9ae571e4c4b863be586d2 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:23 -0500 Subject: [PATCH 099/390] Backport pull request #15757 from jellyfin/release-10.11.z Fix trickplay images using wrong item on alternate versions Original-merge: 481ee03f35d0c5ab625e66f76405edc11a5afb2e Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Controllers/TrickplayController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 2cf66144ce..c9f8b36768 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -86,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController [FromRoute, Required] int index, [FromQuery] Guid? mediaSourceId) { - var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId()); + var item = _libraryManager.GetItemById<BaseItem>(mediaSourceId ?? itemId, User.GetUserId()); if (item is null) { return NotFound(); From afc083e9fa77582ab8f79cf2fb94c9989dadd05b Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:24 -0500 Subject: [PATCH 100/390] Backport pull request #15766 from jellyfin/release-10.11.z Fix backdrop images being deleted when stored with media Original-merge: 12c5d6b63650c34a25609066a0138d37032eb7c2 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Manager/ItemImageProvider.cs | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 75882a088a..e0354dbdfa 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -88,7 +88,15 @@ namespace MediaBrowser.Providers.Manager } } - singular.AddRange(item.GetImages(ImageType.Backdrop)); + foreach (var backdrop in item.GetImages(ImageType.Backdrop)) + { + var imageInMetadataFolder = backdrop.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase); + if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled()) + { + singular.Add(backdrop); + } + } + PruneImages(item, singular); return singular.Count > 0; @@ -466,10 +474,36 @@ namespace MediaBrowser.Providers.Manager } } - if (UpdateMultiImages(item, images, ImageType.Backdrop)) + bool hasBackdrop = false; + bool backdropStoredWithMedia = false; + + foreach (var image in images) { - changed = true; - foundImageTypes.Add(ImageType.Backdrop); + if (image.Type != ImageType.Backdrop) + { + continue; + } + + hasBackdrop = true; + + if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase)) + { + backdropStoredWithMedia = true; + break; + } + } + + if (hasBackdrop) + { + if (UpdateMultiImages(item, images, ImageType.Backdrop)) + { + changed = true; + } + + if (backdropStoredWithMedia) + { + foundImageTypes.Add(ImageType.Backdrop); + } } if (foundImageTypes.Count > 0) From 252ab45473d0a3b5c2f45dee42ced5e6179c6028 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:25 -0500 Subject: [PATCH 101/390] Backport pull request #15767 from jellyfin/release-10.11.z Fix collections display order Original-merge: 22da5187c88a60118cac03bc77427efa72b97888 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/Folder.cs | 7 ------- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 8 +++++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 59a967725f..d2a3290c47 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1406,13 +1406,6 @@ namespace MediaBrowser.Controller.Entities .Where(e => query is null || UserViewBuilder.FilterItem(e, query)) .ToArray(); - if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0)) - { - realChildren = realChildren - .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue) - .ToArray(); - } - var childCount = realChildren.Length; if (result.Count < limit) { diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 1d1fb2c392..3999c3e076 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies if (sortBy == ItemSortBy.Default) { - return items; + return items; } return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); @@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies return Sort(children, user).ToArray(); } + public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null) + { + var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query); + return Sort(children, user).ToArray(); + } + public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { var children = base.GetRecursiveChildren(user, query, out totalCount); From b9158c467a33e86febc60e79f65a3b0ccdbfdd6e Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:26 -0500 Subject: [PATCH 102/390] Backport pull request #15768 from jellyfin/release-10.11.z Fix NullReferenceException in ApplyOrder method Original-merge: b617c62f8ef53848d136155a02e9d3fbffc7b365 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 98072918c4..85ab00a2bf 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1586,14 +1586,14 @@ public sealed class BaseItemRepository private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context) { - var orderBy = filter.OrderBy; + var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray(); var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); if (hasSearch) { - orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy]; + orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy]; } - else if (orderBy.Count == 0) + else if (orderBy.Length == 0) { return query.OrderBy(e => e.SortName); } From 43797fee426dc84ed8093209f76652698d67b3aa Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 28 Dec 2025 07:22:27 -0500 Subject: [PATCH 103/390] Backport pull request #15776 from jellyfin/release-10.11.z Fix AV1 decoding hang regression on RK3588 Original-merge: 035b5895b051edf3f8bb653e52555fb3d63f3544 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 843590a1f4..e088cd358d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -7039,8 +7039,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)) { - var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface); - return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty); + // there's an issue about AV1 AFBC on RK3588, disable it for now until it's fixed upstream + return GetHwaccelType(state, options, "av1", bitDepth, hwSurface); } } From 928a8458dd62007abc0082f478432608e9153f6e Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:28 -0500 Subject: [PATCH 104/390] Backport pull request #15786 from jellyfin/release-10.11.z Fix parental rating filtering with sub-scores Original-merge: 5804d6840c0276d3aef81bfec6af82e496672f01 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/BaseItem.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 3c46d53e5c..d9d2d0e3a8 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1620,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - if (maxAllowedSubRating is not null) + if (!maxAllowedRating.HasValue) { - return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value; + return true; } - return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value; + if (ratingScore.Score != maxAllowedRating.Value) + { + return ratingScore.Score < maxAllowedRating.Value; + } + + return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value; } public ParentalRatingScore GetParentalRatingScore() From 3c802a75054f316e04a5d914036f3164e8bf7e87 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:30 -0500 Subject: [PATCH 105/390] Backport pull request #15793 from jellyfin/release-10.11.z Prefer US rating on fallback Original-merge: 156761405e7fd5308474a7e6301839ae7c694dfa Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Localization/LocalizationManager.cs | 10 +++++++--- .../Localization/LocalizationManagerTests.cs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index b4c65ad85f..d99ad4665e 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -311,15 +311,19 @@ namespace Emby.Server.Implementations.Localization else { // Fall back to server default language for ratings check - // If it has no ratings, use the US ratings - var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); + var ratingsDictionary = GetParentalRatingsDictionary(); if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { return value; } } - // If we don't find anything, check all ratings systems + // If we don't find anything, check all ratings systems, starting with US + if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue)) + { + return usValue; + } + foreach (var dictionary in _allParentalRatings.Values) { if (dictionary.TryGetValue(rating, out var value)) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 6d6bba4fc4..e60522bf78 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -203,6 +203,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Null(localizationManager.GetRatingScore(value)); } + [Theory] + [InlineData("TV-MA", "DE", 17, 1)] // US-only rating, DE country code + [InlineData("PG-13", "FR", 13, 0)] // US-only rating, FR country code + [InlineData("R", "JP", 17, 0)] // US-only rating, JP country code + public async Task GetRatingScore_FallbackPrioritizesUS_Success(string rating, string countryCode, int expectedScore, int? expectedSubScore) + { + var localizationManager = Setup(new ServerConfiguration() + { + MetadataCountryCode = countryCode + }); + await localizationManager.LoadAll(); + + var score = localizationManager.GetRatingScore(rating); + + Assert.NotNull(score); + Assert.Equal(expectedScore, score.Score); + Assert.Equal(expectedSubScore, score.SubScore); + } + [Theory] [InlineData("Default", "Default")] [InlineData("HeaderLiveTV", "Live TV")] From 88acd51ee2b6a30a8a92a9e863fd4aaef48ee590 Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:31 -0500 Subject: [PATCH 106/390] Backport pull request #15807 from jellyfin/release-10.11.z Enforce more strict webm check Original-merge: 8379b4634aeaf9827d07a41cf9ba8fd80c8c323e Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Probing/ProbeResultNormalizer.cs | 9 +- .../Probing/ProbeResultNormalizerTests.cs | 12 ++ .../video_web_like_mkv_with_subtitle.json | 137 ++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 1823496bf1..570ef9ee27 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -300,9 +300,12 @@ namespace MediaBrowser.MediaEncoding.Probing // Handle WebM else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase)) { - // Limit WebM to supported codecs - if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) - || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) + // Limit WebM to supported stream types and codecs. + // FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible. + // Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result. + if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio) + || mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) + || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) { splitFormat[i] = string.Empty; } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 94710a0957..8a2f84734e 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -195,6 +195,18 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.False(res.MediaStreams[0].IsAVC); } + [Fact] + public void GetMediaInfo_WebM_Like_Mkv() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_web_like_mkv_with_subtitle.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); + + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File); + + Assert.Equal("mkv", res.Container); + Assert.Equal(3, res.MediaStreams.Count); + } + [Fact] public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success() { diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json new file mode 100644 index 0000000000..4f52dd90dc --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json @@ -0,0 +1,137 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "vp8", + "codec_long_name": "On2 VP8", + "profile": "1", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 540, + "height": 360, + "coded_width": 540, + "coded_height": 360, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "3:2", + "pix_fmt": "yuv420p", + "level": -99, + "field_order": "progressive", + "refs": 1, + "r_frame_rate": "2997/125", + "avg_frame_rate": "2997/125", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 1, + "codec_name": "vorbis", + "codec_long_name": "Vorbis", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration": "117.707000", + "bit_rate": "127998", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 2, + "codec_name": "subrip", + "codec_long_name": "SubRip subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + } + ], + "format": { + "filename": "sample.mkv", + "nb_streams": 3, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "117.700914", + "size": "8566268", + "bit_rate": "582239", + "probe_score": 100 + } +} From b429306f05820d8502939d305f147df37c25fba0 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 28 Dec 2025 07:22:32 -0500 Subject: [PATCH 107/390] Backport pull request #15819 from jellyfin/release-10.11.z Fix the use of HWA in unsupported H.264 Hi422P/Hi444PP Original-merge: 4c587776d6263698bd0e00b56c06f14d46c4c2ec Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../MediaEncoding/EncodingHelper.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e088cd358d..91d88dc08b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6359,6 +6359,21 @@ 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 (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))) + { + return null; + } + } + } + var decoder = hardwareAccelerationType switch { HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth), From 45e881c93e694a174409fcec6743feef9ccf0b70 Mon Sep 17 00:00:00 2001 From: cvium <cvium@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:33 -0500 Subject: [PATCH 108/390] Backport pull request #15826 from jellyfin/release-10.11.z add CultureDto cache Original-merge: 1805f2259f44aba0ca97ff0de2ad0b0a3614fa03 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Localization/LocalizationManager.cs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index d99ad4665e..bc80c2b405 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -38,6 +38,7 @@ namespace Emby.Server.Implementations.Localization private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly ConcurrentDictionary<string, CultureDto?> _cultureCache = new(StringComparer.OrdinalIgnoreCase); private List<CultureDto> _cultures = []; private FrozenDictionary<string, string> _iso6392BtoT = null!; @@ -161,6 +162,7 @@ namespace Emby.Server.Implementations.Localization list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames)); } + _cultureCache.Clear(); _cultures = list; _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } @@ -169,20 +171,31 @@ namespace Emby.Server.Implementations.Localization /// <inheritdoc /> public CultureDto? FindLanguageInfo(string language) { - // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs - for (var i = 0; i < _cultures.Count; i++) + if (string.IsNullOrEmpty(language)) { - var culture = _cultures[i]; - if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) - || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) - || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase) - || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) - { - return culture; - } + return null; } - return default; + return _cultureCache.GetOrAdd( + language, + static (lang, cultures) => + { + // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs + for (var i = 0; i < cultures.Count; i++) + { + var culture = cultures[i]; + if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) + || lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) + || culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase) + || lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) + { + return culture; + } + } + + return null; + }, + _cultures); } /// <inheritdoc /> From 7f0e71578dc278da36c5efeae2fe2674e4677230 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 28 Dec 2025 07:22:34 -0500 Subject: [PATCH 109/390] Backport pull request #15833 from jellyfin/release-10.11.z Fix missing H.264 and AV1 SDR fallbacks in HLS playlist Original-merge: 01b20d3b75e00909e7a531a625eda5ecb1a76f42 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 112 +++++++++++++---------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index a38ad379cc..16e51151d9 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -154,7 +154,7 @@ public class DynamicHlsHelper // from universal audio service, need to override the AudioCodec when the actual request differs from original query if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase)) { - var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString()); + var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString); newQuery["AudioCodec"] = state.OutputAudioCodec; queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery); } @@ -173,10 +173,21 @@ public class DynamicHlsHelper queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; } - // Main stream - var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + // Video rotation metadata is only supported in fMP4 remuxing + if (state.VideoStream is not null + && state.VideoRequest is not null + && (state.VideoStream?.Rotation ?? 0) != 0 + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) + && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) + { + queryString += "&AllowVideoStreamCopy=false"; + } - playlistUrl += queryString; + // Main stream + var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + var playlistUrl = baseUrl + queryString; + var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString); var subtitleStreams = state.MediaSource .MediaStreams @@ -198,37 +209,36 @@ public class DynamicHlsHelper AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); } - // Video rotation metadata is only supported in fMP4 remuxing - if (state.VideoStream is not null - && state.VideoRequest is not null - && (state.VideoStream?.Rotation ?? 0) != 0 - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) - && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) - { - playlistUrl += "&AllowVideoStreamCopy=false"; - } - var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); if (state.VideoStream is not null && state.VideoRequest is not null) { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - // Provide SDR HEVC entrance for backward compatibility. - if (encodingOptions.AllowHevcEncoding - && !encodingOptions.AllowAv1Encoding - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream.VideoRange == VideoRange.HDR - && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + // Provide AV1 and HEVC SDR entrances for backward compatibility. + foreach (var sdrVideoCodec in new[] { "av1", "hevc" }) { - var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); - if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0) + var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding + && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase); + var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding + && string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase); + var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed; + + if (isEncodingAllowed + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.VideoRange == VideoRange.HDR) { - // Force HEVC Main Profile and disable video stream copy. - state.OutputVideoCodec = "hevc"; - var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main"); - sdrVideoUrl += "&AllowVideoStreamCopy=false"; + // Force AV1 and HEVC Main Profile and disable video stream copy. + state.OutputVideoCodec = sdrVideoCodec; + + var sdrPlaylistQuery = playlistQuery; + sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec; + sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main"; + sdrPlaylistQuery["AllowVideoStreamCopy"] = "false"; + + var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery); // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range. AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup); @@ -238,12 +248,30 @@ public class DynamicHlsHelper } } + // Provide H.264 SDR entrance for backward compatibility. + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.VideoRange == VideoRange.HDR) + { + // Force H.264 and disable video stream copy. + state.OutputVideoCodec = "h264"; + + var sdrPlaylistQuery = playlistQuery; + sdrPlaylistQuery["VideoCodec"] = "h264"; + sdrPlaylistQuery["AllowVideoStreamCopy"] = "false"; + + var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery); + + // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range. + AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup); + + // Restore the video codec + state.OutputVideoCodec = "copy"; + } + // Provide Level 5.0 entrance for backward compatibility. // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // but in fact it is capable of playing videos up to Level 6.1. - if (encodingOptions.AllowHevcEncoding - && !encodingOptions.AllowAv1Encoding - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.VideoStream.Level.HasValue && state.VideoStream.Level > 150 && state.VideoStream.VideoRange == VideoRange.SDR @@ -273,12 +301,15 @@ public class DynamicHlsHelper var variation = GetBitrateVariation(totalBitrate); var newBitrate = totalBitrate - variation; - var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + var variantQuery = playlistQuery; + variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture); + var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); variation *= 2; newBitrate = totalBitrate - variation; - variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture); + variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); } @@ -863,23 +894,6 @@ public class DynamicHlsHelper return variation; } - private string ReplaceVideoBitrate(string url, int oldValue, int newValue) - { - return url.Replace( - "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), - "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), - StringComparison.OrdinalIgnoreCase); - } - - private string ReplaceProfile(string url, string codec, string oldValue, string newValue) - { - string profileStr = codec + "-profile="; - return url.Replace( - profileStr + oldValue, - profileStr + newValue, - StringComparison.OrdinalIgnoreCase); - } - private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue) { var oldPlaylist = playlist.ToString(); From 7af5ee1812a18eb062158618de38b6102c00595f Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 28 Dec 2025 07:22:36 -0500 Subject: [PATCH 110/390] Backport pull request #15834 from jellyfin/release-10.11.z Fix video lacking SAR and DAR are marked as anamorphic Original-merge: 9470439cfa1eaf7cb9717f16031b020cedab516a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Probing/ProbeResultNormalizer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 570ef9ee27..50f7716d86 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -857,7 +857,12 @@ namespace MediaBrowser.MediaEncoding.Probing } // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe - if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) + if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio) + && string.IsNullOrEmpty(streamInfo.DisplayAspectRatio)) + { + stream.IsAnamorphic = false; + } + else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) { stream.IsAnamorphic = false; } From 5c76dd26bc7d2a2a5d039a6b02182ee0bff76992 Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:37 -0500 Subject: [PATCH 111/390] Backport pull request #15835 from jellyfin/release-10.11.z Use hvc1 codectag for Dolby Vision 8.4 Original-merge: 18096e48e0c72b08598a06e5512e6eb81d91fb51 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index fe6f855b5e..1e3e2740f0 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1839,8 +1839,9 @@ public class DynamicHlsController : BaseJellyfinApiController { if (isActualOutputVideoCodecHevc) { - // Prefer dvh1 to dvhe - args += " -tag:v:0 dvh1 -strict -2"; + // Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari. + var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1"; + args += $" -tag:v:0 {codecTag} -strict -2"; } else if (isActualOutputVideoCodecAv1) { From c728e97bda972d24a572d85c56831a3da334f12d Mon Sep 17 00:00:00 2001 From: Collin-Swish <79892877+Collin-Swish@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:38 -0500 Subject: [PATCH 112/390] Backport pull request #15858 from jellyfin/release-10.11.z Fix playlist item de-duplication Original-merge: 78e3702cb064fc664ed1a658ad534cf66f5373d3 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 8df15e4408..e0a4c4f320 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -72,7 +72,7 @@ public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo> } else { - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray(); } if (replaceData || targetItem.Shares.Count == 0) From 336958318d3fa6a917161e5824f1411b19b8f921 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:00:33 +0000 Subject: [PATCH 113/390] Update dependency MetaBrainz.MusicBrainz to v7 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 14ff3fadbe..c78f3061df 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> - <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> + <PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> From 82b2e7773f2caa29e6f3c41e3067cd7e32dee5f6 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 14 Oct 2025 20:56:40 +0200 Subject: [PATCH 114/390] Pass cancellation token to GetReleaseGroupResultAsync --- .../Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index c35324746a..c9cba61b72 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Jellyfin.Extensions; @@ -83,7 +84,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu if (!string.IsNullOrEmpty(releaseGroupId)) { var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false); - return GetReleaseGroupResult(releaseGroupResult.Releases); + + // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable + return GetReleaseGroupResult(releaseGroupResult.Releases, CancellationToken.None); } var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); @@ -128,7 +131,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu } } - private IEnumerable<RemoteSearchResult> GetReleaseGroupResult(IEnumerable<IRelease>? releaseSearchResults) + private IEnumerable<RemoteSearchResult> GetReleaseGroupResult(IEnumerable<IRelease>? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (releaseSearchResults is null) { @@ -138,7 +141,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu foreach (var result in releaseSearchResults) { // Fetch full release info, otherwise artists are missing - var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups); + var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups, cancellationToken); yield return GetReleaseResult(fullResult); } } From 6c8395ff87d91445bf83729d9bac70295f74d230 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 14 Oct 2025 20:50:26 +0200 Subject: [PATCH 115/390] Fix build --- .../Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index c9cba61b72..88c8e4f7c9 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -86,7 +86,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false); // No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable - return GetReleaseGroupResult(releaseGroupResult.Releases, CancellationToken.None); + return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(cancellationToken); } var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId(); @@ -131,7 +131,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu } } - private IEnumerable<RemoteSearchResult> GetReleaseGroupResult(IEnumerable<IRelease>? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default) + private async IAsyncEnumerable<RemoteSearchResult> GetReleaseGroupResultAsync(IEnumerable<IRelease>? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (releaseSearchResults is null) { @@ -141,7 +141,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu foreach (var result in releaseSearchResults) { // Fetch full release info, otherwise artists are missing - var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups, cancellationToken); + var fullResult = await _musicBrainzQuery.LookupReleaseAsync(result.Id, Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false); yield return GetReleaseResult(fullResult); } } From 75f1276119b44e3d6264439acb3bc00d6956f253 Mon Sep 17 00:00:00 2001 From: Joker <jellyfin.pucker@mail.rndmjoker.com> Date: Mon, 29 Dec 2025 05:23:28 -0500 Subject: [PATCH 116/390] 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 278d78ae44..14c42d3c0e 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -61,7 +61,7 @@ "NotificationOptionVideoPlayback": "Video wird abgespielt", "NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt", "Photos": "Fotos", - "Playlists": "Playlists", + "Playlists": "Wiedergabeliste", "Plugin": "Plugin", "PluginInstalledWithName": "{0} wurde installiert", "PluginUninstalledWithName": "{0} wurde deinstalliert", From 2f62a8bb396da66cd6cc51d739308f8f4c703716 Mon Sep 17 00:00:00 2001 From: MrPlow <joachim.huffer@gmail.com> Date: Mon, 29 Dec 2025 10:10:56 -0500 Subject: [PATCH 117/390] 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 14c42d3c0e..0b042c8fed 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -61,7 +61,7 @@ "NotificationOptionVideoPlayback": "Video wird abgespielt", "NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt", "Photos": "Fotos", - "Playlists": "Wiedergabeliste", + "Playlists": "Wiedergabelisten", "Plugin": "Plugin", "PluginInstalledWithName": "{0} wurde installiert", "PluginUninstalledWithName": "{0} wurde deinstalliert", From d28ee6d71415b4c1f5c158f30f427b6952b8d65b Mon Sep 17 00:00:00 2001 From: SamuWhale <themorajr@gmail.com> Date: Mon, 29 Dec 2025 08:44:22 -0500 Subject: [PATCH 118/390] Translated using Weblate (Thai) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/th/ --- Emby.Server.Implementations/Localization/Core/th.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 113e4f30ff..65ddb55e94 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -135,5 +135,7 @@ "TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย", "TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี", "TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment", - "TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay" + "TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay", + "CleanupUserDataTask": "ส่วนงานล้างข้อมูลผู้ใช้", + "CleanupUserDataTaskDescription": "ล้างข้อมูลผู้ใช้ทั้งหมด (สถานะการรับชม สถานะรายการโปรด ฯลฯ) จากสื่อที่ไม่ได้ใช้งานแล้วอย่างน้อย 90 วัน" } From 72b4faa00b743dc5bbd2e25c54b216510e978a5a Mon Sep 17 00:00:00 2001 From: ZeusCraft10 <akhilachanta8@gmail.com> Date: Tue, 30 Dec 2025 17:31:40 -0500 Subject: [PATCH 119/390] 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 e5fb07170888f5c77df9a686b1a73bafae26c784 Mon Sep 17 00:00:00 2001 From: stevenaw <sweerdenburg@gmail.com> Date: Wed, 31 Dec 2025 09:26:31 -0500 Subject: [PATCH 120/390] Optimize StringHelper.ToFirstUpper() --- MediaBrowser.Model/Extensions/StringHelper.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs index 77cbef00f5..58cde8620c 100644 --- a/MediaBrowser.Model/Extensions/StringHelper.cs +++ b/MediaBrowser.Model/Extensions/StringHelper.cs @@ -1,3 +1,5 @@ +using System; + namespace MediaBrowser.Model.Extensions { /// <summary> @@ -25,14 +27,11 @@ namespace MediaBrowser.Model.Extensions return string.Create( str.Length, - str, + str.AsSpan(), (chars, buf) => { chars[0] = char.ToUpperInvariant(buf[0]); - for (int i = 1; i < chars.Length; i++) - { - chars[i] = buf[i]; - } + buf.Slice(1).CopyTo(chars.Slice(1)); }); } } From f08657ab27c2f7db1200a83fa62c3c0aa6b12f67 Mon Sep 17 00:00:00 2001 From: Jonathan Davies <jpds@protonmail.com> Date: Wed, 31 Dec 2025 14:20:41 +0000 Subject: [PATCH 121/390] SessionManager: Log when playback is started --- Emby.Server.Implementations/Session/SessionManager.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index cf2ca047cf..f52e0f6c6c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -793,6 +793,15 @@ namespace Emby.Server.Implementations.Session PlaySessionId = info.PlaySessionId }; + if (info.Item is not null) + { + _logger.LogInformation( + "Playback started reported by app {0} {1} playing {2}", + session.Client, + session.ApplicationVersion, + info.Item.Name); + } + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); // Nothing to save here From b564a43d9c81a5cc032a2def2ab2aac44215a398 Mon Sep 17 00:00:00 2001 From: Jonathan Davies <jpds@protonmail.com> Date: Wed, 31 Dec 2025 14:30:25 +0000 Subject: [PATCH 122/390] SessionManager: Log usernames in playback messages --- Emby.Server.Implementations/Session/SessionManager.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index f52e0f6c6c..7109f3e4f7 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -796,7 +796,8 @@ namespace Emby.Server.Implementations.Session if (info.Item is not null) { _logger.LogInformation( - "Playback started reported by app {0} {1} playing {2}", + "Playback started for user {0} reported by app {1} {2} playing {3}", + session.UserName, session.Client, session.ApplicationVersion, info.Item.Name); @@ -1069,7 +1070,8 @@ namespace Emby.Server.Implementations.Session var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown"; _logger.LogInformation( - "Playback stopped reported by app {0} {1} playing {2}. Stopped at {3} ms", + "Playback stopped for user {0} reported by app {1} {2} playing {3}. Stopped at {4} ms", + session.UserName, session.Client, session.ApplicationVersion, info.Item.Name, From 3c77758b32b5e8ad6517728eb6a3fe25e498b272 Mon Sep 17 00:00:00 2001 From: Jonathan Davies <jpds@protonmail.com> Date: Wed, 31 Dec 2025 20:51:37 +0000 Subject: [PATCH 123/390] SessionManager: Improved wording of playback messages --- .../Session/SessionManager.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 7109f3e4f7..bbe23f8df3 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -796,11 +796,11 @@ namespace Emby.Server.Implementations.Session if (info.Item is not null) { _logger.LogInformation( - "Playback started for user {0} reported by app {1} {2} playing {3}", + "User {0} started playback of '{1}' ({2} {3})", session.UserName, + info.Item.Name, session.Client, - session.ApplicationVersion, - info.Item.Name); + session.ApplicationVersion); } await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); @@ -1070,12 +1070,12 @@ namespace Emby.Server.Implementations.Session var msString = info.PositionTicks.HasValue ? (info.PositionTicks.Value / 10000).ToString(CultureInfo.InvariantCulture) : "unknown"; _logger.LogInformation( - "Playback stopped for user {0} reported by app {1} {2} playing {3}. Stopped at {4} ms", + "User {0} stopped playback of '{1}' at {2}ms ({3} {4})", session.UserName, - session.Client, - session.ApplicationVersion, info.Item.Name, - msString); + msString, + session.Client, + session.ApplicationVersion); } if (info.NowPlayingQueue is not null) From d1055b0b3660d120fed332deb2535986d52d9e0f Mon Sep 17 00:00:00 2001 From: Peaches_MLG <PeachesMLG100@gmail.com> Date: Fri, 2 Jan 2026 01:46:17 +0000 Subject: [PATCH 124/390] Fixed issue with ABI Compatability - Difference not completing (#15924) --- .github/workflows/ci-compat.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index a3c49969ce..2ca101591f 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -106,7 +106,7 @@ jobs: { echo 'body<<EOF' for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do - COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )" + COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 || true )" if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then printf "\n${file}\n${COMPAT_OUTPUT}\n" fi From 23b48a0d0f92706bc4f533cfa78077796ce8da61 Mon Sep 17 00:00:00 2001 From: Tim Eisele <Ghost_of_Stone@web.de> Date: Fri, 2 Jan 2026 02:46:51 +0100 Subject: [PATCH 125/390] Upgrade Swashbuckle and fix OpenAPI spec (#15886) --- Directory.Packages.props | 4 +- .../ApiServiceCollectionExtensions.cs | 35 +----------- .../Filters/AdditionalModelFilter.cs | 9 ---- .../Filters/CachingOpenApiProvider.cs | 16 +++++- .../Filters/FlagsEnumSchemaFilter.cs | 53 +++++++++++++++++++ 5 files changed, 71 insertions(+), 46 deletions(-) create mode 100644 Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 14ff3fadbe..f71027e1a7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,8 +80,8 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Linq.Async" Version="6.0.3" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" /> diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 04dd19eda6..8373fd50fd 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -255,6 +255,7 @@ namespace Jellyfin.Server.Extensions c.AddSwaggerTypeMappings(); c.SchemaFilter<IgnoreEnumSchemaFilter>(); + c.SchemaFilter<FlagsEnumSchemaFilter>(); c.OperationFilter<RetryOnTemporarilyUnavailableFilter>(); c.OperationFilter<SecurityRequirementsOperationFilter>(); c.OperationFilter<FileResponseFilter>(); @@ -342,25 +343,6 @@ namespace Jellyfin.Server.Extensions } }); - /* - * Support BlurHash dictionary - */ - options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() => - new OpenApiSchema - { - Type = "object", - Properties = typeof(ImageType).GetEnumNames().ToDictionary( - name => name, - _ => new OpenApiSchema - { - Type = "object", - AdditionalProperties = new OpenApiSchema - { - Type = "string" - } - }) - }); - // Support dictionary with nullable string value. options.MapType<Dictionary<string, string?>>(() => new OpenApiSchema @@ -373,21 +355,6 @@ namespace Jellyfin.Server.Extensions } }); - // Manually describe Flags enum. - options.MapType<TranscodeReason>(() => - new OpenApiSchema - { - Type = "array", - Items = new OpenApiSchema - { - Reference = new OpenApiReference - { - Id = nameof(TranscodeReason), - Type = ReferenceType.Schema, - } - } - }); - // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. options.MapType<Version>(() => new OpenApiSchema { diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 58d37db5a5..7407bd2eb7 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -225,15 +225,6 @@ namespace Jellyfin.Server.Filters context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository); } - - context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema - { - Type = "string", - Enum = Enum.GetNames<TranscodeReason>() - .Select(e => new OpenApiString(e)) - .Cast<IOpenApiAny>() - .ToArray() - }); } } } diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index b560ec50ef..833b684444 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -2,6 +2,7 @@ using System; using AsyncKeyedLock; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Swagger; @@ -23,6 +24,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider private readonly IMemoryCache _memoryCache; private readonly SwaggerGenerator _swaggerGenerator; private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions; + private readonly ILogger<CachingOpenApiProvider> _logger; /// <summary> /// Initializes a new instance of the <see cref="CachingOpenApiProvider"/> class. @@ -31,15 +33,18 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider /// <param name="apiDescriptionsProvider">The api descriptions provider.</param> /// <param name="schemaGenerator">The schema generator.</param> /// <param name="memoryCache">The memory cache.</param> + /// <param name="logger">The logger.</param> public CachingOpenApiProvider( IOptions<SwaggerGeneratorOptions> optionsAccessor, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, ISchemaGenerator schemaGenerator, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + ILogger<CachingOpenApiProvider> logger) { _swaggerGeneratorOptions = optionsAccessor.Value; _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); _memoryCache = memoryCache; + _logger = logger; } /// <inheritdoc /> @@ -61,7 +66,16 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider throw new InvalidOperationException("OpenApi document is generating"); } + try + { openApiDocument = _swaggerGenerator.GetSwagger(documentName); + } + catch (Exception ex) + { + _logger.LogError(ex, "OpenAPI generation error"); + throw; + } + _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); return AdjustDocument(openApiDocument, host, basePath); } diff --git a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs new file mode 100644 index 0000000000..3e0b69d017 --- /dev/null +++ b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// <summary> +/// Schema filter to ensure flags enums are represented correctly in OpenAPI. +/// </summary> +/// <remarks> +/// For flags enums: +/// - The enum schema definition is set to type "string" (not integer). +/// - Properties using flags enums are transformed to arrays referencing the enum schema. +/// </remarks> +public class FlagsEnumSchemaFilter : ISchemaFilter +{ + /// <inheritdoc /> + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type); + if (type is null || !type.IsEnum) + { + return; + } + + // Check if enum has [Flags] attribute + if (!type.IsDefined(typeof(FlagsAttribute), false)) + { + return; + } + + if (context.MemberInfo is null) + { + // Processing the enum definition itself - ensure it's type "string" not "integer" + schema.Type = "string"; + schema.Format = null; + } + else + { + // Processing a property that uses the flags enum - transform to array + // Generate the enum schema to ensure it exists in the repository + var enumSchema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); + + // Flags enums should be represented as arrays referencing the enum schema + // since multiple values can be combined + schema.Type = "array"; + schema.Format = null; + schema.Enum = null; + schema.AllOf = null; + schema.Items = enumSchema; + } + } +} From 85ff70859711c9be7a4aa271d5a90464eb790de4 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 29 Dec 2025 17:51:50 +0100 Subject: [PATCH 126/390] Format workflows --- .github/workflows/ci-codeql-analysis.yml | 3 +++ .github/workflows/ci-openapi.yml | 18 ++++++++++++++---- .github/workflows/commands.yml | 3 +++ .github/workflows/issue-template-check.yml | 3 +++ .github/workflows/project-automation.yml | 1 + 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 5ca3edd7ba..6d4f4edb69 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -21,6 +21,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: @@ -31,7 +32,9 @@ jobs: with: languages: ${{ matrix.language }} queries: +security-extended + - name: Autobuild uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 46af68e587..95c0facf38 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -20,12 +20,15 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} + - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: @@ -46,6 +49,7 @@ jobs: 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 }} @@ -54,12 +58,15 @@ jobs: 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@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: @@ -84,22 +91,27 @@ jobs: with: name: openapi-head path: openapi-head + - name: Download openapi-base uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: openapi-base path: openapi-base + - name: Workaround openapi-diff issue run: | sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json + - name: Calculate OpenAPI difference + id: openapi-diff uses: docker://openapitools/openapi-diff continue-on-error: true with: args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json - - id: read-diff - name: Read openapi-diff output + + - name: Read openapi-diff output + id: read-diff run: | # Read and fix markdown body=$(cat openapi-changes.md) @@ -178,7 +190,6 @@ jobs: username: "${{ secrets.REPO_USER }}" key: "${{ secrets.REPO_KEY }}" debug: false - script_stop: false script: | if ! test -d /run/workflows; then sudo mkdir -p /run/workflows @@ -240,7 +251,6 @@ jobs: username: "${{ secrets.REPO_USER }}" key: "${{ secrets.REPO_KEY }}" debug: false - script_stop: false script: | if ! test -d /run/workflows; then sudo mkdir -p /run/workflows diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 0d3e09d1a1..a70ec00eef 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -43,13 +43,16 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: jellyfin/jellyfin-triage-script + - name: install python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.14' cache: 'pip' + - name: install python packages run: pip install -r rename/requirements.txt + - name: run rename script run: python3 rename.py working-directory: ./rename diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 8be48b5c3a..53a66e013e 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -13,13 +13,16 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: jellyfin/jellyfin-triage-script + - name: install python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.14' cache: 'pip' + - name: install python packages run: pip install -r main-repo-triage/requirements.txt + - name: check and comment issue working-directory: ./main-repo-triage run: python3 single_issue_gha.py diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index d62f655b30..7b29d3c817 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -21,6 +21,7 @@ jobs: with: project: Current Release action: delete + column: In progress repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add to 'Release Next' project From 18dc32d73532b2e703635cf9e3161df21dc47613 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Thu, 1 Jan 2026 22:27:50 +0100 Subject: [PATCH 127/390] Fix OpenAPI diff workflow --- .github/workflows/ci-openapi.yml | 65 ++++---------------------------- 1 file changed, 8 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 95c0facf38..ab3c907e52 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -77,7 +77,7 @@ jobs: openapi-diff: permissions: - pull-requests: write # to create or update comment (peter-evans/create-or-update-comment) + pull-requests: write name: OpenAPI - Difference if: ${{ github.event_name == 'pull_request_target' }} @@ -98,65 +98,16 @@ jobs: name: openapi-base path: openapi-base - - name: Workaround openapi-diff issue - run: | - sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json - sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json - - - name: Calculate OpenAPI difference + - name: Detect OpenAPI changes id: openapi-diff - uses: docker://openapitools/openapi-diff - continue-on-error: true + uses: Shadowghost/openapi-diff-action@v1.0.0 with: - args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json + 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 }} - - name: Read openapi-diff output - id: read-diff - run: | - # Read and fix markdown - body=$(cat openapi-changes.md) - # Write to workflow summary - echo "$body" >> $GITHUB_STEP_SUMMARY - # Set ApiChanged var - if [ "$body" != '' ]; then - echo "ApiChanged=1" >> "$GITHUB_OUTPUT" - else - echo "ApiChanged=0" >> "$GITHUB_OUTPUT" - fi - # Add header/footer for diff comment - echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md - echo "<details>" >> openapi-changes-reply.md - echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md - echo "" >> openapi-changes-reply.md - echo "$body" >> openapi-changes-reply.md - echo "" >> openapi-changes-reply.md - echo "</details>" >> openapi-changes-reply.md - - name: Find difference comment - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 - id: find-comment - with: - issue-number: ${{ github.event.pull_request.number }} - direction: last - body-includes: openapi-diff-workflow-comment - - name: Reply or edit difference comment (changed) - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - if: ${{ steps.read-diff.outputs.ApiChanged == '1' }} - with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find-comment.outputs.comment-id }} - edit-mode: replace - body-path: openapi-changes-reply.md - - name: Edit difference comment (unchanged) - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 - if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }} - with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find-comment.outputs.comment-id }} - edit-mode: replace - body: | - <!--openapi-diff-workflow-comment--> - - No changes to OpenAPI specification found. See history of this comment for previous changes. publish-unstable: name: OpenAPI - Publish Unstable Spec From e02a2ae48fa43252e6747221b8e2161a99dda4c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:49:18 +0000 Subject: [PATCH 128/390] Update dependency AsyncKeyedLock to v8 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f71027e1a7..5aa4f13386 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ </PropertyGroup> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <ItemGroup Label="Package Dependencies"> - <PackageVersion Include="AsyncKeyedLock" Version="7.1.8" /> + <PackageVersion Include="AsyncKeyedLock" Version="8.0.0" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" /> From bfae788a44d7c5640bd23c6f433533ef16f30107 Mon Sep 17 00:00:00 2001 From: Dzmitry Zubialevich <dzmitry.zubialevich@gmail.com> Date: Fri, 2 Jan 2026 05:40:57 -0500 Subject: [PATCH 129/390] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 29847048cb..4d769efc33 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -95,7 +95,7 @@ "ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску", "Shows": "Шоу", "StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.", - "SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}", + "SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}", "TvShows": "Тэлепраграма", "Undefined": "Нявызначана", "UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны", @@ -114,7 +114,7 @@ "TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.", "TaskRefreshChapterImages": "Вынуць выявы раздзелаў", "TaskRefreshLibrary": "Сканаваць бібліятэку", - "TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.", + "TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метаданыя.", "TaskCleanLogs": "Ачысціць журнал", "TaskRefreshPeople": "Абнавіць выканаўцаў", "TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.", @@ -137,5 +137,5 @@ "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", - "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." + "CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." } From 8a1129bbdec24f60b011ae1f90bb0c41b989d378 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Mon, 5 Jan 2026 12:19:26 +0100 Subject: [PATCH 130/390] Use new repoository and lock to commit --- .github/workflows/ci-openapi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index ab3c907e52..8406d1d2d6 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -100,7 +100,7 @@ jobs: - name: Detect OpenAPI changes id: openapi-diff - uses: Shadowghost/openapi-diff-action@v1.0.0 + uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0 with: old-spec: openapi-base/openapi.json new-spec: openapi-head/openapi.json From 90390772869495228793e1b29db20c8741a309de Mon Sep 17 00:00:00 2001 From: ZeusCraft10 <akhilachanta8@gmail.com> Date: Mon, 5 Jan 2026 06:22:44 -0500 Subject: [PATCH 131/390] Fix zh-CN subtitle language display The DisplayTitle property was using .NET's CultureInfo.GetCultures(NeutralCultures) to resolve language display names. Since zh-CN is a specific culture (not neutral), it would fall back to the base 'zh' code, resulting in generic 'Chinese' instead of 'Chinese (Simplified)'. This change adds a LocalizedLanguage property to MediaStream that gets populated via LocalizationManager.FindLanguageInfo() when streams are retrieved from the database. This leverages Jellyfin's existing iso6392.txt mappings which correctly map zh-CN to 'Chinese (Simplified)'. The same pattern is already used for other localized strings like LocalizedDefault and LocalizedExternal. --- .../Item/MediaStreamRepository.cs | 6 +++ MediaBrowser.Model/Entities/MediaStream.cs | 52 +++---------------- .../Entities/MediaStreamTests.cs | 43 +++++++++++++++ 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 7eb13b7408..64874ccad7 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -158,6 +158,12 @@ public class MediaStreamRepository : IMediaStreamRepository dto.LocalizedDefault = _localization.GetLocalizedString("Default"); dto.LocalizedExternal = _localization.GetLocalizedString("External"); + if (!string.IsNullOrEmpty(dto.Language)) + { + var culture = _localization.FindLanguageInfo(dto.Language); + dto.LocalizedLanguage = culture?.DisplayName; + } + if (dto.Type is MediaStreamType.Subtitle) { dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index b1626e2c98..c443af32cf 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -260,6 +260,8 @@ namespace MediaBrowser.Model.Entities public string LocalizedHearingImpaired { get; set; } + public string LocalizedLanguage { get; set; } + public string DisplayTitle { get @@ -273,29 +275,8 @@ namespace MediaBrowser.Model.Entities // Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded). if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase)) { - // Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified). - var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); - CultureInfo match = null; - if (Language.Contains('-', StringComparison.OrdinalIgnoreCase)) - { - match = cultures.FirstOrDefault(r => - r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase)); - - if (match is null) - { - string baseLang = Language.AsSpan().LeftPart('-').ToString(); - match = cultures.FirstOrDefault(r => - r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase)); - } - } - else - { - match = cultures.FirstOrDefault(r => - r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)); - } - - string fullLanguage = match?.DisplayName; - attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); + // Use pre-resolved localized language name, falling back to raw language code. + attributes.Add(StringHelper.FirstToUpper(LocalizedLanguage ?? Language)); } if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) @@ -393,29 +374,8 @@ namespace MediaBrowser.Model.Entities if (!string.IsNullOrEmpty(Language)) { - // Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified). - var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); - CultureInfo match = null; - if (Language.Contains('-', StringComparison.OrdinalIgnoreCase)) - { - match = cultures.FirstOrDefault(r => - r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase)); - - if (match is null) - { - string baseLang = Language.AsSpan().LeftPart('-').ToString(); - match = cultures.FirstOrDefault(r => - r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase)); - } - } - else - { - match = cultures.FirstOrDefault(r => - r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)); - } - - string fullLanguage = match?.DisplayName; - attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); + // Use pre-resolved localized language name, falling back to raw language code. + attributes.Add(StringHelper.FirstToUpper(LocalizedLanguage ?? Language)); } else { diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs index f4c0d9fe8f..c1a3a45445 100644 --- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs @@ -108,6 +108,49 @@ namespace Jellyfin.Model.Tests.Entities IsExternal = true }); + // Test LocalizedLanguage is used when set (fixes zh-CN display issue #15935) + data.Add( + "Chinese (Simplified) - SRT", + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = null, + Language = "zh-CN", + LocalizedLanguage = "Chinese (Simplified)", + IsForced = false, + IsDefault = false, + Codec = "SRT" + }); + + // Test LocalizedLanguage for audio streams + data.Add( + "Japanese - AAC - Stereo", + new MediaStream + { + Type = MediaStreamType.Audio, + Title = null, + Language = "jpn", + LocalizedLanguage = "Japanese", + IsForced = false, + IsDefault = false, + Codec = "AAC", + ChannelLayout = "stereo" + }); + + // Test fallback to Language when LocalizedLanguage is null + data.Add( + "Eng - ASS", + new MediaStream + { + Type = MediaStreamType.Subtitle, + Title = null, + Language = "eng", + LocalizedLanguage = null, + IsForced = false, + IsDefault = false, + Codec = "ASS" + }); + return data; } From 4138214ac326618635248b6f84b5338166b68b1d Mon Sep 17 00:00:00 2001 From: Dzmitry Zubialevich <dzmitry.zubialevich@gmail.com> Date: Mon, 5 Jan 2026 04:34:05 -0500 Subject: [PATCH 132/390] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 4d769efc33..62ada96c00 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -16,7 +16,7 @@ "Collections": "Калекцыі", "Default": "Па змаўчанні", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", - "Folders": "Тэчкі", + "Folders": "Папкі", "Favorites": "Абранае", "External": "Знешні", "Genres": "Жанры", @@ -104,7 +104,7 @@ "UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}", "UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}", "ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку", - "ValueSpecialEpisodeName": "Спецэпізод - {0}", + "ValueSpecialEpisodeName": "Спецвыпуск - {0}", "VersionNumber": "Версія {0}", "TasksMaintenanceCategory": "Абслугоўванне", "TasksLibraryCategory": "Бібліятэка", From 0ff869dfcd4ab527dccc975c9be414d1c050a90d Mon Sep 17 00:00:00 2001 From: ZeusCraft10 <akhilachanta8@gmail.com> Date: Mon, 5 Jan 2026 21:08:26 -0500 Subject: [PATCH 133/390] fix: Handle unknown item types gracefully in DeserializeBaseItem When querying items with recursive=true, items with types from removed plugins would cause a 500 error. Now these items are skipped with a warning log instead of throwing an exception. Fixes #15945 --- .../Item/BaseItemRepository.cs | 40 ++++++----- .../Migrations/Routines/MigrateLibraryDb.cs | 7 +- .../Item/BaseItemRepositoryTests.cs | 72 +++++++++++++++++++ 3 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 85ab00a2bf..b7f1c23e0c 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -277,7 +277,7 @@ public sealed class BaseItemRepository dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyNavigations(dbQuery, filter); - result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); + result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!; result.StartIndex = filter.StartIndex ?? 0; return result; } @@ -297,7 +297,7 @@ public sealed class BaseItemRepository dbQuery = ApplyQueryPaging(dbQuery, filter); dbQuery = ApplyNavigations(dbQuery, filter); - return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); + return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!; } /// <inheritdoc/> @@ -341,7 +341,7 @@ public sealed class BaseItemRepository mainquery = ApplyNavigations(mainquery, filter); - return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); + return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!; } /// <inheritdoc /> @@ -1159,7 +1159,7 @@ public sealed class BaseItemRepository return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null; } - private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false) + private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false) { ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity)); if (_serverConfigurationManager?.Configuration is null) @@ -1182,11 +1182,19 @@ public sealed class BaseItemRepository /// <param name="logger">Logger.</param> /// <param name="appHost">The application server Host.</param> /// <param name="skipDeserialization">If only mapping should be processed.</param> - /// <returns>A mapped BaseItem.</returns> - /// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception> - public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false) + /// <returns>A mapped BaseItem, or null if the item type is unknown.</returns> + public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false) { - var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type."); + var type = GetType(baseItemEntity.Type); + if (type is null) + { + logger.LogWarning( + "Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database corruption.", + baseItemEntity.Id, + baseItemEntity.Type); + return null; + } + BaseItemDto? dto = null; if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization) { @@ -1353,10 +1361,9 @@ public sealed class BaseItemRepository .. resultQuery .AsEnumerable() .Where(e => e is not null) - .Select(e => - { - return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount); - }) + .Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount)) + .Where(e => e.Item is not null) + .Select(e => (e.Item!, e.itemCount)) ]; } else @@ -1367,10 +1374,9 @@ public sealed class BaseItemRepository .. query .AsEnumerable() .Where(e => e is not null) - .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e => - { - return (DeserializeBaseItem(e, filter.SkipDeserialization), null); - }) + .Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)null)) + .Where(e => e.Item is not null) + .Select(e => (e.Item!, e.ItemCounts)) ]; } @@ -2671,6 +2677,6 @@ public sealed class BaseItemRepository .Where(e => artistNames.Contains(e.Name)) .ToArray(); - return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray()); + return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray()); } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index d221d18531..4b1e53a355 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1247,8 +1247,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine } var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false); - var dataKeys = baseItem.GetUserDataKeys(); - userDataKeys.AddRange(dataKeys); + if (baseItem is not null) + { + var dataKeys = baseItem.GetUserDataKeys(); + userDataKeys.AddRange(dataKeys); + } return (entity, userDataKeys.ToArray()); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs new file mode 100644 index 0000000000..c450cbb0e1 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs @@ -0,0 +1,72 @@ +using System; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Item; + +public class BaseItemRepositoryTests +{ + [Fact] + public void DeserializeBaseItem_WithUnknownType_ReturnsNull() + { + // Arrange + var entity = new BaseItemEntity + { + Id = Guid.NewGuid(), + Type = "NonExistent.Plugin.CustomItemType" + }; + + // Act + var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false); + + // Assert + Assert.Null(result); + } + + [Fact] + public void DeserializeBaseItem_WithUnknownType_LogsWarning() + { + // Arrange + var entity = new BaseItemEntity + { + Id = Guid.NewGuid(), + Type = "NonExistent.Plugin.CustomItemType" + }; + var loggerMock = new Mock<ILogger>(); + + // Act + BaseItemRepository.DeserializeBaseItem(entity, loggerMock.Object, null, false); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny<EventId>(), + It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unknown type", StringComparison.OrdinalIgnoreCase)), + It.IsAny<Exception?>(), + It.IsAny<Func<It.IsAnyType, Exception?, string>>()), + Times.Once); + } + + [Fact] + public void DeserializeBaseItem_WithKnownType_ReturnsItem() + { + // Arrange + var entity = new BaseItemEntity + { + Id = Guid.NewGuid(), + Type = "MediaBrowser.Controller.Entities.Movies.Movie" + }; + + // Act + var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false); + + // Assert + Assert.NotNull(result); + } +} From 244757c92cae8dc1cb12dfb4a4e976bbfd7e751d Mon Sep 17 00:00:00 2001 From: ZeusCraft10 <akhilachanta8@gmail.com> Date: Mon, 5 Jan 2026 23:03:22 -0500 Subject: [PATCH 134/390] Fix KeyNotFoundException in CryptographyProvider.Verify When a password hash is missing the 'iterations' parameter, Verify now throws a descriptive FormatException instead of KeyNotFoundException. - Extract GetIterationsParameter() helper method to avoid code duplication - Provide distinct error messages for missing vs invalid parameters - Add comprehensive unit tests for CryptographyProvider --- CONTRIBUTORS.md | 1 + .../Cryptography/CryptographyProvider.cs | 27 ++++- .../Cryptography/CryptographyProviderTests.cs | 102 ++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0fd509f842..171509382d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -209,6 +209,7 @@ - [Kirill Nikiforov](https://github.com/allmazz) - [bjorntp](https://github.com/bjorntp) - [martenumberto](https://github.com/martenumberto) + - [ZeusCraft10](https://github.com/ZeusCraft10) # Emby Contributors diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 5380c45d84..0381c4d355 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -39,22 +39,24 @@ namespace Emby.Server.Implementations.Cryptography { if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal)) { + var iterations = GetIterationsParameter(hash); return hash.Hash.SequenceEqual( Rfc2898DeriveBytes.Pbkdf2( password, hash.Salt, - int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture), + iterations, HashAlgorithmName.SHA1, 32)); } if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal)) { + var iterations = GetIterationsParameter(hash); return hash.Hash.SequenceEqual( Rfc2898DeriveBytes.Pbkdf2( password, hash.Salt, - int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture), + iterations, HashAlgorithmName.SHA512, DefaultOutputLength)); } @@ -62,6 +64,27 @@ namespace Emby.Server.Implementations.Cryptography throw new NotSupportedException($"Can't verify hash with id: {hash.Id}"); } + /// <summary> + /// Extracts and validates the iterations parameter from a password hash. + /// </summary> + /// <param name="hash">The password hash containing parameters.</param> + /// <returns>The number of iterations.</returns> + /// <exception cref="FormatException">Thrown when iterations parameter is missing or invalid.</exception> + private static int GetIterationsParameter(PasswordHash hash) + { + if (!hash.Parameters.TryGetValue("iterations", out var iterationsStr)) + { + throw new FormatException($"Password hash with id '{hash.Id}' is missing required 'iterations' parameter."); + } + + if (!int.TryParse(iterationsStr, CultureInfo.InvariantCulture, out var iterations)) + { + throw new FormatException($"Password hash with id '{hash.Id}' has invalid 'iterations' parameter: '{iterationsStr}'."); + } + + return iterations; + } + /// <inheritdoc /> public byte[] GenerateSalt() => GenerateSalt(DefaultSaltLength); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs new file mode 100644 index 0000000000..052bdf740e --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Cryptography/CryptographyProviderTests.cs @@ -0,0 +1,102 @@ +using System; +using Emby.Server.Implementations.Cryptography; +using MediaBrowser.Model.Cryptography; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Cryptography; + +public class CryptographyProviderTests +{ + private readonly CryptographyProvider _sut = new(); + + [Fact] + public void CreatePasswordHash_WithPassword_ReturnsHashWithIterations() + { + var hash = _sut.CreatePasswordHash("testpassword"); + + Assert.Equal("PBKDF2-SHA512", hash.Id); + Assert.True(hash.Parameters.ContainsKey("iterations")); + Assert.NotEmpty(hash.Salt.ToArray()); + Assert.NotEmpty(hash.Hash.ToArray()); + } + + [Fact] + public void Verify_WithValidPassword_ReturnsTrue() + { + const string password = "testpassword"; + var hash = _sut.CreatePasswordHash(password); + + Assert.True(_sut.Verify(hash, password)); + } + + [Fact] + public void Verify_WithWrongPassword_ReturnsFalse() + { + var hash = _sut.CreatePasswordHash("correctpassword"); + + Assert.False(_sut.Verify(hash, "wrongpassword")); + } + + [Fact] + public void Verify_PBKDF2_MissingIterations_ThrowsFormatException() + { + var hash = PasswordHash.Parse("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"); + + var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password")); + Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void Verify_PBKDF2SHA512_MissingIterations_ThrowsFormatException() + { + var hash = PasswordHash.Parse("$PBKDF2-SHA512$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"); + + var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password")); + Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void Verify_PBKDF2_InvalidIterationsFormat_ThrowsFormatException() + { + var hash = PasswordHash.Parse("$PBKDF2$iterations=abc$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"); + + var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password")); + Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void Verify_PBKDF2SHA512_InvalidIterationsFormat_ThrowsFormatException() + { + var hash = PasswordHash.Parse("$PBKDF2-SHA512$iterations=notanumber$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"); + + var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password")); + Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void Verify_UnsupportedHashId_ThrowsNotSupportedException() + { + var hash = PasswordHash.Parse("$UNKNOWN$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D"); + + Assert.Throws<NotSupportedException>(() => _sut.Verify(hash, "password")); + } + + [Fact] + public void GenerateSalt_ReturnsNonEmptyArray() + { + var salt = _sut.GenerateSalt(); + + Assert.NotEmpty(salt); + } + + [Theory] + [InlineData(16)] + [InlineData(32)] + [InlineData(64)] + public void GenerateSalt_WithLength_ReturnsArrayOfSpecifiedLength(int length) + { + var salt = _sut.GenerateSalt(length); + + Assert.Equal(length, salt.Length); + } +} From 582a1d98665eba7f7d7b510633cc283fd72305e1 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Tue, 6 Jan 2026 23:15:06 +0800 Subject: [PATCH 135/390] Add TrueHD and DTS codes string for HLS Signed-off-by: nyanmisaka <nst799610810@gmail.com> --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 17 ++++++- Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs | 47 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 16e51151d9..44e1c6d5a2 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -754,7 +754,9 @@ public class DynamicHlsHelper { if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { - string? profile = state.GetRequestedProfiles("aac").FirstOrDefault(); + string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) + ? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault(); + return HlsCodecStringHelpers.GetAACString(profile); } @@ -788,6 +790,19 @@ public class DynamicHlsHelper return HlsCodecStringHelpers.GetOPUSString(); } + if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)) + { + return HlsCodecStringHelpers.GetTRUEHDString(); + } + + if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)) + { + // lavc only support encoding DTS core profile + string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS"; + + return HlsCodecStringHelpers.GetDTSString(profile); + } + return string.Empty; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 0efb7f45de..cf42d5f10b 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -41,6 +41,11 @@ public static class HlsCodecStringHelpers /// </summary> public const string OPUS = "Opus"; + /// <summary> + /// Codec name for TRUEHD. + /// </summary> + public const string TRUEHD = "mlpa"; + /// <summary> /// Gets a MP3 codec string. /// </summary> @@ -59,7 +64,7 @@ public static class HlsCodecStringHelpers { StringBuilder result = new StringBuilder("mp4a", 9); - if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(profile, "HE-AAC", StringComparison.OrdinalIgnoreCase)) { result.Append(".40.5"); } @@ -117,6 +122,46 @@ public static class HlsCodecStringHelpers return OPUS; } + /// <summary> + /// Gets an TRUEHD codec string. + /// </summary> + /// <returns>TRUEHD codec string.</returns> + public static string GetTRUEHDString() + { + return TRUEHD; + } + + /// <summary> + /// Gets an DTS codec string. + /// </summary> + /// <param name="profile">DTS profile.</param> + /// <returns>DTS codec string.</returns> + public static string GetDTSString(string? profile) + { + if (string.Equals(profile, "DTS", StringComparison.OrdinalIgnoreCase) + || string.Equals(profile, "DTS-ES", StringComparison.OrdinalIgnoreCase) + || string.Equals(profile, "DTS 96/24", StringComparison.OrdinalIgnoreCase)) + { + return "dtsc"; + } + + if (string.Equals(profile, "DTS-HD HRA", StringComparison.OrdinalIgnoreCase) + || string.Equals(profile, "DTS-HD MA", StringComparison.OrdinalIgnoreCase) + || string.Equals(profile, "DTS-HD MA + DTS:X", StringComparison.OrdinalIgnoreCase) + || string.Equals(profile, "DTS-HD MA + DTS:X IMAX", StringComparison.OrdinalIgnoreCase)) + { + return "dtsh"; + } + + if (string.Equals(profile, "DTS Express", StringComparison.OrdinalIgnoreCase)) + { + return "dtse"; + } + + // Default to DTS core if profile is invalid + return "dtsc"; + } + /// <summary> /// Gets a H.264 codec string. /// </summary> From 103f556c8df6abfe2962ede8e7936292c46add4d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:48:42 +0000 Subject: [PATCH 136/390] Update dependency z440.atl.core to 7.10.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c7a7346f37..31b46da614 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,7 +88,7 @@ <PackageVersion Include="System.Text.Json" Version="9.0.11" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.9.0" /> + <PackageVersion Include="z440.atl.core" Version="7.10.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> From e233eee07b318ee59d528e715ad0ba490013b1db Mon Sep 17 00:00:00 2001 From: SilentSkies <toby.taylor400@outlook.com> Date: Fri, 9 Jan 2026 17:14:09 -0500 Subject: [PATCH 137/390] Translated using Weblate (Welsh) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cy/ --- Emby.Server.Implementations/Localization/Core/cy.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 794a8e4ce4..e1ec9d22bf 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}", "Books": "Llyfrau", "AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus", - "Artists": "Artistiaid", + "Artists": "Crewyr", "AppDeviceValues": "Ap: {0}, Dyfais: {1}", "Albums": "Albwmau", "Genres": "Genres", @@ -67,7 +67,7 @@ "NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain", "MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru", "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru", - "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}", + "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}", "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau", "UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}", "UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}", @@ -123,5 +123,8 @@ "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod", "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.", "TaskCleanCache": "Gwaghau Ffolder Cache", - "HearingImpaired": "Nam ar y clyw" + "HearingImpaired": "Nam ar y clyw", + "TaskAudioNormalization": "Gwastatau Sain", + "TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.", + "TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay" } From 22ee5113d0273031094fa5b9dc83fb29d46c88c2 Mon Sep 17 00:00:00 2001 From: SilentSkies <toby.taylor400@outlook.com> Date: Fri, 9 Jan 2026 17:22:26 -0500 Subject: [PATCH 138/390] Translated using Weblate (Welsh) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cy/ --- Emby.Server.Implementations/Localization/Core/cy.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index e1ec9d22bf..3f10b6adc3 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -126,5 +126,8 @@ "HearingImpaired": "Nam ar y clyw", "TaskAudioNormalization": "Gwastatau Sain", "TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.", - "TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay" + "TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay", + "TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.", + "TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll", + "TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon" } From c4f4dcc181f300f0b74b2e4b259c9fdedb4f71ef Mon Sep 17 00:00:00 2001 From: SilentSkies <toby.taylor400@outlook.com> Date: Fri, 9 Jan 2026 17:33:27 -0500 Subject: [PATCH 139/390] Translated using Weblate (Welsh) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cy/ --- Emby.Server.Implementations/Localization/Core/cy.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 3f10b6adc3..d9ebd13f07 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -129,5 +129,8 @@ "TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay", "TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.", "TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll", - "TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon" + "TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon", + "TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae", + "TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.", + "TaskExtractMediaSegments": "Sganio Darnau Cyfryngau" } From 0ee872999d4f24374af698d6b593743ccf4d6538 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:11:25 -0500 Subject: [PATCH 140/390] Backport pull request #15931 from jellyfin/release-10.11.z Fix tag inheritance for Continue Watching queries Original-merge: 559e0088e5316a857f764a848e76e4fbd62fa834 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b7f1c23e0c..3b3d3c4f43 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2495,35 +2495,24 @@ public sealed class BaseItemRepository if (filter.ExcludeInheritedTags.Length > 0) { + var excludedTags = filter.ExcludeInheritedTags; baseQuery = baseQuery.Where(e => - !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)) - && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue || - !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)))); + !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue)) + && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue)))); } if (filter.IncludeInheritedTags.Length > 0) { - // For seasons and episodes, we also need to check the parent series' tags. - if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season)) - { - baseQuery = baseQuery.Where(e => - e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)))); - } + var includeTags = filter.IncludeInheritedTags; + var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist; + baseQuery = baseQuery.Where(e => + e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)) - // A playlist should be accessible to its owner regardless of allowed tags. - else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) - { - baseQuery = baseQuery.Where(e => - e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); - // d ^^ this is stupid it hate this. - } - else - { - baseQuery = baseQuery.Where(e => - e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); - } + // For seasons and episodes, we also need to check the parent series' tags. + || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))) + + // A playlist should be accessible to its owner regardless of allowed tags + || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""))); } if (filter.SeriesStatuses.Length > 0) From d270957c8254a24d8c073ad9d1d9316e950538ed Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:11:26 -0500 Subject: [PATCH 141/390] Backport pull request #15950 from jellyfin/release-10.11.z Revert "always sort season by index number" Original-merge: 32d2414de0b3d119929c063714b6e4f0023893c7 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/TV/Series.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 427c2995bc..6396631f99 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; query.IncludeItemTypes = new[] { BaseItemKind.Season }; - query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) }; + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; if (user is not null && !user.DisplayMissingEpisodes) { @@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; + if (query.OrderBy.Count == 0) + { + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; + } if (query.IncludeItemTypes.Length == 0) { From cf9051c27773ffa764a785fddd045b001f5861ad Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:11:27 -0500 Subject: [PATCH 142/390] Backport pull request #15961 from jellyfin/release-10.11.z Fix crash when plugin repository has an invalid URL Original-merge: 317a3a47c374fc4cb58f4c7a537b33fabb4c764f Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Updates/InstallationManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 5ff4001601..5f9e29b563 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest); return Array.Empty<PackageInfo>(); } + catch (NotSupportedException ex) + { + _logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest); + return Array.Empty<PackageInfo>(); + } catch (HttpRequestException ex) { _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest); From 62e51fd00a429cf06e72e49c1e94cbd499fd366a Mon Sep 17 00:00:00 2001 From: Samuvel Paul <samuvalpaul@outlook.com> Date: Sat, 10 Jan 2026 06:39:07 -0500 Subject: [PATCH 143/390] Translated using Weblate (Malayalam) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ml/ --- Emby.Server.Implementations/Localization/Core/ml.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index 5c34493816..8c20ded3ac 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -2,12 +2,12 @@ "AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}", "Application": "അപ്ലിക്കേഷൻ", "AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു", - "CameraImageUploadedFrom": "Camera 0 from എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു", + "CameraImageUploadedFrom": "{0} എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്‌ലോഡുചെയ്‌തു", "ChapterNameValue": "അധ്യായം {0}", "DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു", "DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു", "FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു", - "Forced": "നിർബന്ധിച്ചു", + "Forced": "നിർബന്ധിതമായി", "HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ", "HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ", "HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ", @@ -114,7 +114,7 @@ "Artists": "കലാകാരന്മാർ", "Shows": "ഷോകൾ", "Default": "സ്ഥിരസ്ഥിതി", - "Favorites": "പ്രിയങ്കരങ്ങൾ", + "Favorites": "പ്രിയപ്പെട്ടവ", "Books": "പുസ്തകങ്ങൾ", "Genres": "വിഭാഗങ്ങൾ", "Channels": "ചാനലുകൾ", From c464ba83f2cb034424f63426e579572fa23b05c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:53:28 +0000 Subject: [PATCH 144/390] Update dependency dotnet-ef to v10 --- .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 029a48f6a1..1d65527d98 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.11", + "version": "10.0.2", "commands": [ "dotnet-ef" ] From 0892847c2f52534b2b0f9bd04d24965704dd5bc7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:34:32 +0000 Subject: [PATCH 145/390] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 8 ++++---- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 4 ++-- .github/workflows/ci-tests.yml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 6d4f4edb69..d5f724f3bc 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -23,18 +23,18 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Setup .NET - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 2ca101591f..5205d34360 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@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: '9.0.x' @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 8406d1d2d6..487791c076 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@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: '9.0.x' @@ -60,7 +60,7 @@ jobs: git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index f70243221d..f607cc52a1 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 + - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: dotnet-version: ${{ env.SDK_VERSION }} From 9e480f6efb4bc0e1f0d1323ed7ed5a7208fded99 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Tue, 11 Nov 2025 17:41:46 +0100 Subject: [PATCH 146/390] Update to .NET 10.0 --- .editorconfig | 7 +++ .github/workflows/ci-openapi.yml | 4 +- .vscode/launch.json | 6 +-- Directory.Packages.props | 47 ++++++++----------- Emby.Naming/Emby.Naming.csproj | 2 +- Emby.Photos/Emby.Photos.csproj | 2 +- .../Emby.Server.Implementations.csproj | 3 +- Jellyfin.Api/Helpers/HlsHelpers.cs | 10 +--- Jellyfin.Api/Jellyfin.Api.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- .../FullSystemBackup/BackupService.cs | 15 +++--- .../Jellyfin.Server.Implementations.csproj | 3 +- .../ApiServiceCollectionExtensions.cs | 11 ++--- Jellyfin.Server/Jellyfin.Server.csproj | 5 +- .../MediaBrowser.Common.csproj | 7 +-- MediaBrowser.Common/Net/NetworkConstants.cs | 1 - MediaBrowser.Common/Net/NetworkUtils.cs | 10 ++-- .../MediaBrowser.Controller.csproj | 4 +- .../MediaBrowser.LocalMetadata.csproj | 2 +- .../MediaBrowser.MediaEncoding.csproj | 3 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 5 +- MediaBrowser.Model/Net/IPData.cs | 5 +- .../MediaBrowser.Providers.csproj | 3 +- .../MediaBrowser.XbmcMetadata.csproj | 2 +- README.md | 4 +- .../Emby.Server.Implementations.Fuzz.csproj | 2 +- fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh | 2 +- .../Jellyfin.Api.Fuzz.csproj | 2 +- fuzz/Jellyfin.Api.Fuzz/fuzz.sh | 2 +- global.json | 2 +- .../Jellyfin.Database.Implementations.csproj | 2 +- .../Locking/OptimisticLockBehavior.cs | 2 + .../Locking/PessimisticLockBehavior.cs | 1 + .../Jellyfin.Database.Providers.Sqlite.csproj | 2 +- .../Jellyfin.Drawing.Skia.csproj | 2 +- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 2 +- .../Jellyfin.Extensions.csproj | 2 +- src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj | 3 +- .../Jellyfin.MediaEncoding.Hls.csproj | 5 +- .../Jellyfin.MediaEncoding.Keyframes.csproj | 6 +-- .../Jellyfin.Networking.csproj | 2 +- .../Manager/NetworkManager.cs | 21 ++++----- tests/Directory.Build.props | 2 +- .../Jellyfin.Api.Tests.csproj | 1 - .../Jellyfin.LiveTv.Tests.csproj | 2 +- .../Jellyfin.Server.Integration.Tests.csproj | 1 - .../Jellyfin.Server.Tests.csproj | 1 - .../ParseNetworkTests.cs | 7 ++- 48 files changed, 99 insertions(+), 140 deletions(-) diff --git a/.editorconfig b/.editorconfig index 313b02563d..fa679f1200 100644 --- a/.editorconfig +++ b/.editorconfig @@ -379,6 +379,9 @@ dotnet_diagnostic.CA1720.severity = suggestion # disable warning CA1724: Type names should not match namespaces dotnet_diagnostic.CA1724.severity = suggestion +# disable warning CA1873: Avoid potentially expensive logging +dotnet_diagnostic.CA1873.severity = suggestion + # disable warning CA1805: Do not initialize unnecessarily dotnet_diagnostic.CA1805.severity = suggestion @@ -400,6 +403,10 @@ dotnet_diagnostic.CA1861.severity = suggestion # disable warning CA2000: Dispose objects before losing scope dotnet_diagnostic.CA2000.severity = suggestion +# TODO: Reevaluate when false positives are fixed: https://github.com/dotnet/roslyn-analyzers/issues/7699 +# disable warning CA2025: Do not pass 'IDisposable' instances into unawaited tasks +dotnet_diagnostic.CA2025.severity = suggestion + # disable warning CA2253: Named placeholders should not be numeric values dotnet_diagnostic.CA2253.severity = suggestion diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 8406d1d2d6..cf2a2868d5 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -35,7 +35,7 @@ jobs: name: openapi-head retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json openapi-base: name: OpenAPI - BASE @@ -73,7 +73,7 @@ jobs: name: openapi-base retention-days: 14 if-no-files-found: error - path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json + path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json openapi-diff: permissions: diff --git a/.vscode/launch.json b/.vscode/launch.json index d97d8de843..681f068b9b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "args": [], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", @@ -22,7 +22,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "args": ["--nowebclient"], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", @@ -34,7 +34,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net10.0/jellyfin.dll", "args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"], "cwd": "${workspaceFolder}/Jellyfin.Server", "console": "internalConsole", diff --git a/Directory.Packages.props b/Directory.Packages.props index 31b46da614..d78e2d021d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,32 +26,27 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.11" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> @@ -81,12 +76,8 @@ <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.2.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" /> - <PackageVersion Include="System.Globalization" Version="4.3.0" /> - <PackageVersion Include="System.Linq.Async" Version="6.0.3" /> - <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.11" /> - <PackageVersion Include="System.Text.Json" Version="9.0.11" /> - <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> + <PackageVersion Include="System.Text.Json" Version="10.0.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.10.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index b84c961165..97b52e42af 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index 645a74aea4..3faeae3803 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -19,7 +19,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 15843730e9..f312fb4db9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -27,7 +27,6 @@ <PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="prometheus-net.DotNetRuntime" /> @@ -39,7 +38,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index cad8d650e9..15540338b3 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -45,15 +45,9 @@ public static class HlsHelpers using var reader = new StreamReader(fileStream); var count = 0; - while (!reader.EndOfStream) + string? line; + while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null) { - var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (line is null) - { - // Nothing currently in buffer. - break; - } - if (line.Contains("#EXTINF:", StringComparison.OrdinalIgnoreCase)) { count++; diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 25feaa2d75..3ccf7a746b 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index fd852ece93..f7660f35dd 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 70483c36cc..30094a88c0 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -102,7 +102,7 @@ public class BackupService : IBackupService } BackupManifest? manifest; - var manifestStream = zipArchiveEntry.Open(); + var manifestStream = await zipArchiveEntry.OpenAsync().ConfigureAwait(false); await using (manifestStream.ConfigureAwait(false)) { manifest = await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false); @@ -160,7 +160,7 @@ public class BackupService : IBackupService } HistoryRow[] historyEntries; - var historyArchive = historyEntry.Open(); + var historyArchive = await historyEntry.OpenAsync().ConfigureAwait(false); await using (historyArchive.ConfigureAwait(false)) { historyEntries = await JsonSerializer.DeserializeAsync<HistoryRow[]>(historyArchive).ConfigureAwait(false) ?? @@ -204,7 +204,7 @@ public class BackupService : IBackupService continue; } - var zipEntryStream = zipEntry.Open(); + var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false); await using (zipEntryStream.ConfigureAwait(false)) { _logger.LogInformation("Restore backup of {Table}", entityType.Type.Name); @@ -329,7 +329,7 @@ public class BackupService : IBackupService _logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName); var zipEntry = zipArchive.CreateEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.SourceName}.json"))); var entities = 0; - var zipEntryStream = zipEntry.Open(); + var zipEntryStream = await zipEntry.OpenAsync().ConfigureAwait(false); await using (zipEntryStream.ConfigureAwait(false)) { var jsonSerializer = new Utf8JsonWriter(zipEntryStream); @@ -366,7 +366,7 @@ public class BackupService : IBackupService foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly) .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly))) { - zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))); + await zipArchive.CreateEntryFromFileAsync(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))).ConfigureAwait(false); } void CopyDirectory(string source, string target, string filter = "*") @@ -380,6 +380,7 @@ public class BackupService : IBackupService foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories)) { + // TODO: @bond make async zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); } } @@ -405,7 +406,7 @@ public class BackupService : IBackupService CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); } - var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open(); + var manifestStream = await zipArchive.CreateEntry(ManifestEntryName).OpenAsync().ConfigureAwait(false); await using (manifestStream.ConfigureAwait(false)) { await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false); @@ -505,7 +506,7 @@ public class BackupService : IBackupService return null; } - var manifestStream = manifestEntry.Open(); + var manifestStream = await manifestEntry.OpenAsync().ConfigureAwait(false); await using (manifestStream.ConfigureAwait(false)) { return await JsonSerializer.DeserializeAsync<BackupManifest>(manifestStream, _serializerSettings).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 6693ab8dbd..4f0c377229 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -27,7 +27,6 @@ <ItemGroup> <PackageReference Include="AsyncKeyedLock" /> - <PackageReference Include="System.Linq.Async" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> </ItemGroup> diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 8373fd50fd..c7bcda442c 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -174,7 +174,7 @@ namespace Jellyfin.Server.Extensions if (config.KnownProxies.Length == 0) { options.ForwardedHeaders = ForwardedHeaders.None; - options.KnownNetworks.Clear(); + options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); } else @@ -184,7 +184,7 @@ namespace Jellyfin.Server.Extensions } // Only set forward limit if we have some known proxies or some known networks. - if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0) + if (options.KnownProxies.Count != 0 || options.KnownIPNetworks.Count != 0) { options.ForwardLimit = null; } @@ -290,10 +290,7 @@ namespace Jellyfin.Server.Extensions } else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet)) { - if (subnet is not null) - { - AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); - } + AddIPAddress(config, options, subnet.BaseAddress, subnet.PrefixLength); } else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { @@ -323,7 +320,7 @@ namespace Jellyfin.Server.Extensions } else { - options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength)); + options.KnownIPNetworks.Add(new System.Net.IPNetwork(addr, prefixLength)); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 14ab114fb4..9f5bf01a05 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <ServerGarbageCollection>false</ServerGarbageCollection> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> @@ -44,9 +44,6 @@ <ItemGroup> <PackageReference Include="CommandLineParser" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" /> <PackageReference Include="Morestachio" /> <PackageReference Include="prometheus-net" /> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 5f15f845c5..c128c2b6bb 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -18,17 +18,12 @@ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" /> - </ItemGroup> - <ItemGroup> <Compile Include="..\SharedVersion.cs" /> </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/MediaBrowser.Common/Net/NetworkConstants.cs b/MediaBrowser.Common/Net/NetworkConstants.cs index ccef5d2719..cec996a1aa 100644 --- a/MediaBrowser.Common/Net/NetworkConstants.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -1,5 +1,4 @@ using System.Net; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Common.Net; diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 24ed47a81b..9c9a35a166 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -6,7 +6,6 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Common.Net; @@ -196,7 +195,7 @@ public static partial class NetworkUtils /// <param name="result">An <see cref="IPNetwork"/>.</param> /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) + public static bool TryParseToSubnet(ReadOnlySpan<char> value, out IPNetwork result, bool negated = false) { // If multiple IP addresses are in a comma-separated string, the individual addresses may contain leading and/or trailing whitespace value = value.Trim(); @@ -210,7 +209,7 @@ public static partial class NetworkUtils if (isAddressNegated != negated) { - result = null; + result = default; return false; } @@ -235,7 +234,7 @@ public static partial class NetworkUtils } } - result = null; + result = default; return false; } @@ -330,7 +329,7 @@ public static partial class NetworkUtils /// <returns>The broadcast address.</returns> public static IPAddress GetBroadcastAddress(IPNetwork network) { - var addressBytes = network.Prefix.GetAddressBytes(); + var addressBytes = network.BaseAddress.GetAddressBytes(); uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); uint broadCastIPAddress = ipAddress | ~ipMaskV4; @@ -347,7 +346,6 @@ public static partial class NetworkUtils public static bool SubnetContainsAddress(IPNetwork network, IPAddress address) { ArgumentNullException.ThrowIfNull(address); - ArgumentNullException.ThrowIfNull(network); if (address.IsIPv4MappedToIPv6) { diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index b5d14e94b1..0025080cc9 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -19,9 +19,7 @@ <ItemGroup> <PackageReference Include="BitFaster.Caching" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" /> - <PackageReference Include="System.Threading.Tasks.Dataflow" /> </ItemGroup> <ItemGroup> @@ -36,7 +34,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 8e3c8cf7f4..c3c26085c3 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -11,7 +11,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index be7eeda929..fc11047a7f 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -26,7 +26,6 @@ <PackageReference Include="BDInfo" /> <PackageReference Include="libse" /> <PackageReference Include="Microsoft.Extensions.Http" /> - <PackageReference Include="System.Text.Encoding.CodePages" /> <PackageReference Include="UTF.Unknown" /> </ItemGroup> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index ef025d02dc..c655c4ccb3 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -14,7 +14,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> @@ -37,13 +37,10 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> <PackageReference Include="MimeTypes"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="System.Globalization" /> - <PackageReference Include="System.Text.Json" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index c116d883ed..e016ffea10 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Sockets; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace MediaBrowser.Model.Net; @@ -66,9 +65,9 @@ public class IPData { if (Address.Equals(IPAddress.None)) { - return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) + return Subnet.BaseAddress.AddressFamily.Equals(IPAddress.None) ? AddressFamily.Unspecified - : Subnet.Prefix.AddressFamily; + : Subnet.BaseAddress.AddressFamily; } else { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 34b3104b0b..ed0c63b97f 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -18,7 +18,6 @@ <PackageReference Include="AsyncKeyedLock" /> <PackageReference Include="LrcParser" /> <PackageReference Include="MetaBrainz.MusicBrainz" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="Newtonsoft.Json" /> @@ -28,7 +27,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index b195af96cf..cfb3533f35 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -15,7 +15,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/README.md b/README.md index 9830e8e9c8..e546e7f115 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ --- -Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET platform to enable full cross-platform support. +Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET platform to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team that wants to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! @@ -133,7 +133,7 @@ A second option is to build the project and then run the resulting executable fi ```bash dotnet build # Build the project -cd Jellyfin.Server/bin/Debug/net9.0 # Change into the build output directory +cd Jellyfin.Server/bin/Debug/net10.0 # Change into the build output directory ``` 2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj index 1373d2fe05..1ac7402f9c 100644 --- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj +++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> </PropertyGroup> <ItemGroup> diff --git a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh index 8183bb37ad..771aa66770 100755 --- a/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh +++ b/fuzz/Emby.Server.Implementations.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Emby.Server.Implementations.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net9.0/Emby.Server.Implementations.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net10.0/Emby.Server.Implementations.Fuzz "$1" diff --git a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj index 04c7be11d5..dad2f8e4e7 100644 --- a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj +++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> </PropertyGroup> <ItemGroup> diff --git a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh index 15148e1bb2..537de905d7 100755 --- a/fuzz/Jellyfin.Api.Fuzz/fuzz.sh +++ b/fuzz/Jellyfin.Api.Fuzz/fuzz.sh @@ -8,4 +8,4 @@ cp bin/Jellyfin.Api.dll . dotnet build mkdir -p Findings -AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net9.0/Jellyfin.Api.Fuzz "$1" +AFL_SKIP_BIN_CHECK=1 afl-fuzz -i "Testcases/$1" -o "Findings/$1" -t 5000 ./bin/Debug/net10.0/Jellyfin.Api.Fuzz "$1" diff --git a/global.json b/global.json index 2e13a6387d..867a4cfa08 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestMinor" } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj index 28c4972d21..0b29a71cbd 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs index 7bcc7eeca4..76ffa5a9ea 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1873 + using System; using System.Data.Common; using System.Linq; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs index 2d6bc69028..404292e8eb 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs @@ -1,5 +1,6 @@ #pragma warning disable MT1013 // Releasing lock without guarantee of execution #pragma warning disable MT1012 // Acquiring lock without guarantee of releasing +#pragma warning disable CA1873 using System; using System.Data; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj index 03e5fc4958..aeee527016 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index ba402dfe09..f7c20463f0 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <!-- TODO: Remove once we update SkiaSharp > 2.88.5 --> diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj index 5f4b3fe8d4..a442f74576 100644 --- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj +++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj @@ -6,7 +6,7 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index f52fd014da..9a7cf4aabe 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <PublishRepositoryUrl>true</PublishRepositoryUrl> diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj index f04c02504c..575441de92 100644 --- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj +++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -13,7 +13,6 @@ <ItemGroup> <PackageReference Include="AsyncKeyedLock" /> <PackageReference Include="Jellyfin.XmlTv" /> - <PackageReference Include="System.Linq.Async" /> </ItemGroup> <ItemGroup> diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index 80b5aa84e4..902f513768 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -12,9 +12,6 @@ <ProjectReference Include="../Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj" /> </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" /> - </ItemGroup> <ItemGroup> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index cc8d942ebb..5e7e2090cd 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -22,10 +22,6 @@ <PackageReference Include="NEbml" /> </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> - </ItemGroup> - <ItemGroup> <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> <_Parameter1>Jellyfin.MediaEncoding.Keyframes.Tests</_Parameter1> diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj index 1a146549de..36b9581a7b 100644 --- a/src/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 126d9f15cf..ed7b6dfdea 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -16,7 +16,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Jellyfin.Networking.Manager; @@ -376,7 +375,7 @@ public class NetworkManager : INetworkManager, IDisposable if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) { var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) - ? network.Prefix + ? network.BaseAddress : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) .FirstOrDefault() ?? IPAddress.None)) @@ -545,7 +544,7 @@ public class NetworkManager : INetworkManager, IDisposable { foreach (var lan in _lanSubnets) { - var lanPrefix = lan.Prefix; + var lanPrefix = lan.BaseAddress; publishedServerUrls.Add( new PublishedServerUriOverride( new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), @@ -554,9 +553,9 @@ public class NetworkManager : INetworkManager, IDisposable false)); } } - else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) + else if (NetworkUtils.TryParseToSubnet(identifier, out var result)) { - var data = new IPData(result.Prefix, result); + var data = new IPData(result.BaseAddress, result); publishedServerUrls.Add( new PublishedServerUriOverride( data, @@ -623,7 +622,7 @@ public class NetworkManager : INetworkManager, IDisposable var parts = details.Split(','); if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { - var address = subnet.Prefix; + var address = subnet.BaseAddress; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -920,7 +919,7 @@ public class NetworkManager : INetworkManager, IDisposable { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IsInLocalNetwork(subnet.Prefix); + return IsInLocalNetwork(subnet.BaseAddress); } return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled) @@ -1171,13 +1170,13 @@ public class NetworkManager : INetworkManager, IDisposable var logLevel = debug ? LogLevel.Debug : LogLevel.Information; if (_logger.IsEnabled(logLevel)) { - _logger.Log(logLevel, "Defined LAN subnets: {Subnets}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Defined LAN exclusions: {Subnets}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Used LAN subnets: {Subnets}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN subnets: {Subnets}", _lanSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {Subnets}", _excludedSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Used LAN subnets: {Subnets}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.BaseAddress + "/" + s.PrefixLength)); _logger.Log(logLevel, "Filtered interface addresses: {Addresses}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _logger.Log(logLevel, "Bind Addresses {Addresses}", GetAllBindInterfaces(false).OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _logger.Log(logLevel, "Remote IP filter is {Type}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); - _logger.Log(logLevel, "Filtered subnets: {Subnets}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Filtered subnets: {Subnets}", _remoteAddressFilter.Select(s => s.BaseAddress + "/" + s.PrefixLength)); } } } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 6b851021f9..feec35307c 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -4,7 +4,7 @@ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 0150189108..6b84c4438f 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -10,7 +10,6 @@ <PackageReference Include="AutoFixture.AutoMoq" /> <PackageReference Include="AutoFixture.Xunit2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> - <PackageReference Include="Microsoft.Extensions.Options" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="xunit" /> <PackageReference Include="xunit.runner.visualstudio"> diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj index fdcf7d61e0..bdf6bc383a 100644 --- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj +++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj @@ -1,6 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> </PropertyGroup> <ItemGroup> diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 8228c0df70..7b0e23788b 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -5,7 +5,6 @@ <PackageReference Include="AutoFixture.AutoMoq" /> <PackageReference Include="AutoFixture.Xunit2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> - <PackageReference Include="Microsoft.Extensions.Options" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="xunit" /> <PackageReference Include="xunit.runner.visualstudio"> diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 5fea805ae1..21596e0ed2 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -5,7 +5,6 @@ <PackageReference Include="AutoFixture.AutoMoq" /> <PackageReference Include="AutoFixture.Xunit2" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> - <PackageReference Include="Microsoft.Extensions.Options" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="xunit" /> <PackageReference Include="xunit.runner.visualstudio"> diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 123266d298..14f4c33b6b 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Jellyfin.Server.Tests { @@ -87,7 +86,7 @@ namespace Jellyfin.Server.Tests // Need this here as ::1 and 127.0.0.1 are in them by default. options.KnownProxies.Clear(); - options.KnownNetworks.Clear(); + options.KnownIPNetworks.Clear(); ApiServiceCollectionExtensions.AddProxyAddresses(settings, hostList, options); @@ -97,10 +96,10 @@ namespace Jellyfin.Server.Tests Assert.True(options.KnownProxies.Contains(item)); } - Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count); + Assert.Equal(knownNetworks.Length, options.KnownIPNetworks.Count); foreach (var item in knownNetworks) { - Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.Prefix) && x.PrefixLength == item.PrefixLength)); + Assert.NotEqual(default, options.KnownIPNetworks.FirstOrDefault(x => x.BaseAddress.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength)); } } From 1ba8e2c93c2906682050c95957649c20e1b557d9 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sun, 16 Nov 2025 18:59:50 +0100 Subject: [PATCH 147/390] Fix tests --- .../ApiServiceCollectionExtensions.cs | 2 +- MediaBrowser.Common/Net/NetworkUtils.cs | 27 ++++++++++--------- .../LimitedConcurrencyLibraryScheduler.cs | 2 +- .../Manager/NetworkManager.cs | 27 ++++++++----------- .../NetworkParseTests.cs | 8 +++--- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index c7bcda442c..9df24fa0d7 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -290,7 +290,7 @@ namespace Jellyfin.Server.Extensions } else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet)) { - AddIPAddress(config, options, subnet.BaseAddress, subnet.PrefixLength); + AddIPAddress(config, options, subnet.Address, subnet.Subnet.PrefixLength); } else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 9c9a35a166..5c854b39d5 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; +using MediaBrowser.Model.Net; namespace MediaBrowser.Common.Net; @@ -166,7 +167,7 @@ public static partial class NetworkUtils /// <param name="result">Collection of <see cref="IPNetwork"/>.</param> /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPNetwork>? result, bool negated = false) + public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPData>? result, bool negated = false) { if (values is null || values.Length == 0) { @@ -174,28 +175,28 @@ public static partial class NetworkUtils return false; } - var tmpResult = new List<IPNetwork>(); + List<IPData>? tmpResult = null; for (int a = 0; a < values.Length; a++) { if (TryParseToSubnet(values[a], out var innerResult, negated)) { - tmpResult.Add(innerResult); + (tmpResult ??= new()).Add(innerResult); } } result = tmpResult; - return tmpResult.Count > 0; + return result is not null; } /// <summary> - /// Try parsing a string into an <see cref="IPNetwork"/>, respecting exclusions. - /// Inputs without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP. + /// Try parsing a string into an <see cref="IPData"/>, respecting exclusions. + /// Inputs without a subnet mask will be represented as <see cref="IPData"/> with a single IP. /// </summary> /// <param name="value">Input string to be parsed.</param> - /// <param name="result">An <see cref="IPNetwork"/>.</param> + /// <param name="result">An <see cref="IPData"/>.</param> /// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param> /// <returns><c>True</c> if parsing was successful.</returns> - public static bool TryParseToSubnet(ReadOnlySpan<char> value, out IPNetwork result, bool negated = false) + public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPData? result, bool negated = false) { // If multiple IP addresses are in a comma-separated string, the individual addresses may contain leading and/or trailing whitespace value = value.Trim(); @@ -213,10 +214,12 @@ public static partial class NetworkUtils return false; } - if (value.Contains('/')) + var index = value.IndexOf('/'); + if (index != -1) { - if (IPNetwork.TryParse(value, out result)) + if (IPAddress.TryParse(value[..index], out var address) && IPNetwork.TryParse(value, out var subnet)) { + result = new IPData(address, subnet); return true; } } @@ -224,12 +227,12 @@ public static partial class NetworkUtils { if (address.AddressFamily == AddressFamily.InterNetwork) { - result = address.Equals(IPAddress.Any) ? NetworkConstants.IPv4Any : new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize); + result = address.Equals(IPAddress.Any) ? new IPData(IPAddress.Any, NetworkConstants.IPv4Any) : new IPData(address, new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize)); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { - result = address.Equals(IPAddress.IPv6Any) ? NetworkConstants.IPv6Any : new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize); + result = address.Equals(IPAddress.IPv6Any) ? new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any) : new IPData(address, new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize)); return true; } } diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs index 2811a081aa..6da398129a 100644 --- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs +++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs @@ -188,7 +188,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr await item.Worker(item.Data).ConfigureAwait(true); } - catch (System.Exception ex) + catch (Exception ex) { _logger.LogError(ex, "Error while performing a library operation"); } diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index ed7b6dfdea..9127606bab 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -340,12 +340,12 @@ public class NetworkManager : INetworkManager, IDisposable } else { - _lanSubnets = lanSubnets; + _lanSubnets = lanSubnets.Select(x => x.Subnet).ToArray(); } _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true) - ? excludedSubnets - : new List<IPNetwork>(); + ? excludedSubnets.Select(x => x.Subnet).ToArray() + : Array.Empty<IPNetwork>(); } } @@ -375,7 +375,7 @@ public class NetworkManager : INetworkManager, IDisposable if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) { var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) - ? network.BaseAddress + ? network.Address : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Address) .FirstOrDefault() ?? IPAddress.None)) @@ -444,7 +444,7 @@ public class NetworkManager : INetworkManager, IDisposable var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray(); if (NetworkUtils.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false)) { - remoteAddressFilter = remoteAddressFilterResult.ToList(); + remoteAddressFilter = remoteAddressFilterResult.Select(x => x.Subnet).ToList(); } // Parse everything else as an IP and construct subnet with a single IP @@ -555,10 +555,9 @@ public class NetworkManager : INetworkManager, IDisposable } else if (NetworkUtils.TryParseToSubnet(identifier, out var result)) { - var data = new IPData(result.BaseAddress, result); publishedServerUrls.Add( new PublishedServerUriOverride( - data, + result, replacement, true, true)); @@ -620,16 +619,12 @@ public class NetworkManager : INetworkManager, IDisposable foreach (var details in interfaceList) { var parts = details.Split(','); - if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) + if (NetworkUtils.TryParseToSubnet(parts[0], out var data)) { - var address = subnet.BaseAddress; - var index = int.Parse(parts[1], CultureInfo.InvariantCulture); - if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) + data.Index = int.Parse(parts[1], CultureInfo.InvariantCulture); + if (data.AddressFamily == AddressFamily.InterNetwork || data.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet, parts[2]) - { - Index = index - }; + data.Name = parts[2]; interfaces.Add(data); } } @@ -919,7 +914,7 @@ public class NetworkManager : INetworkManager, IDisposable { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IsInLocalNetwork(subnet.BaseAddress); + return IsInLocalNetwork(subnet.Address); } return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled) diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 38208476f8..871604514b 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -113,7 +113,7 @@ namespace Jellyfin.Networking.Tests public void IPv4SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); - Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress))); } /// <summary> @@ -131,7 +131,7 @@ namespace Jellyfin.Networking.Tests public void IPv4SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { var ipa = IPAddress.Parse(ipAddress); - Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress))); } /// <summary> @@ -147,7 +147,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] public void IPv6SubnetMaskMatchesValidIPAddress(string netMask, string ipAddress) { - Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.True(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] @@ -158,7 +158,7 @@ namespace Jellyfin.Networking.Tests [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] public void IPv6SubnetMaskDoesNotMatchInvalidIPAddress(string netMask, string ipAddress) { - Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Contains(IPAddress.Parse(ipAddress))); + Assert.False(NetworkUtils.TryParseToSubnet(netMask, out var subnet) && subnet.Subnet.Contains(IPAddress.Parse(ipAddress))); } [Theory] From 098e8c6fed6aa1fd873f255b09b58e4780c087d6 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sun, 16 Nov 2025 19:31:45 +0100 Subject: [PATCH 148/390] Replace AlphanumericComparator with new CompareOptions.NumericOrdering --- Emby.Naming/Video/VideoListResolver.cs | 17 ++- .../Sorting/StudioComparer.cs | 4 +- .../Sorting/SortExtensions.cs | 4 +- .../AlphanumericComparator.cs | 112 ------------------ .../AlphanumericComparatorTests.cs | 34 ------ 5 files changed, 18 insertions(+), 153 deletions(-) delete mode 100644 src/Jellyfin.Extensions/AlphanumericComparator.cs delete mode 100644 tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index a3134f3f68..4247fea0e5 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; @@ -136,19 +137,27 @@ namespace Emby.Naming.Video if (videos.Count > 1) { - var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); + var groups = videos + .Select(x => (filename: x.Files[0].FileNameWithoutExtension.ToString(), value: x)) + .Select(x => (x.filename, resolutionMatch: ResolutionRegex().Match(x.filename), x.value)) + .GroupBy(x => x.resolutionMatch.Success) + .ToList(); + videos.Clear(); + + StringComparer comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); foreach (var group in groups) { if (group.Key) { videos.InsertRange(0, group - .OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator()) - .ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + .OrderByDescending(x => x.resolutionMatch.Value, comparer) + .ThenBy(x => x.filename, comparer) + .Select(x => x.value)); } else { - videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator())); + videos.AddRange(group.OrderBy(x => x.filename, comparer).Select(x => x.value)); } } } diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index 0edffb783b..6d041cf112 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,11 +1,11 @@ #pragma warning disable CS1591 using System; +using System.Globalization; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; -using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Sorting ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); - return AlphanumericComparator.CompareValues(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault()); + return CultureInfo.InvariantCulture.CompareInfo.Compare(x.Studios.FirstOrDefault(), y.Studios.FirstOrDefault(), CompareOptions.NumericOrdering); } } } diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index f9c0d39ddd..ec8878dcb2 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,7 +1,9 @@ #pragma warning disable CS1591 using System; +using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Jellyfin.Extensions; @@ -9,7 +11,7 @@ namespace MediaBrowser.Controller.Sorting { public static class SortExtensions { - private static readonly AlphanumericComparator _comparer = new AlphanumericComparator(); + private static readonly StringComparer _comparer = StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.NumericOrdering); public static IEnumerable<T> OrderByString<T>(this IEnumerable<T> list, Func<T, string> getName) { diff --git a/src/Jellyfin.Extensions/AlphanumericComparator.cs b/src/Jellyfin.Extensions/AlphanumericComparator.cs deleted file mode 100644 index 299e2f94ae..0000000000 --- a/src/Jellyfin.Extensions/AlphanumericComparator.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Jellyfin.Extensions -{ - /// <summary> - /// Alphanumeric <see cref="IComparer{T}" />. - /// </summary> - public class AlphanumericComparator : IComparer<string?> - { - /// <summary> - /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other. - /// </summary> - /// <param name="s1">The first object to compare.</param> - /// <param name="s2">The second object to compare.</param> - /// <returns>A signed integer that indicates the relative values of <c>x</c> and <c>y</c>.</returns> - public static int CompareValues(string? s1, string? s2) - { - if (s1 is null && s2 is null) - { - return 0; - } - - if (s1 is null) - { - return -1; - } - - if (s2 is null) - { - return 1; - } - - int len1 = s1.Length; - int len2 = s2.Length; - - // Early return for empty strings - if (len1 == 0 && len2 == 0) - { - return 0; - } - - if (len1 == 0) - { - return -1; - } - - if (len2 == 0) - { - return 1; - } - - int pos1 = 0; - int pos2 = 0; - - do - { - int start1 = pos1; - int start2 = pos2; - - bool isNum1 = char.IsDigit(s1[pos1++]); - bool isNum2 = char.IsDigit(s2[pos2++]); - - while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1) - { - pos1++; - } - - while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2) - { - pos2++; - } - - var span1 = s1.AsSpan(start1, pos1 - start1); - var span2 = s2.AsSpan(start2, pos2 - start2); - - if (isNum1 && isNum2) - { - // Trim leading zeros so we can compare the length - // of the strings to find the largest number - span1 = span1.TrimStart('0'); - span2 = span2.TrimStart('0'); - var span1Len = span1.Length; - var span2Len = span2.Length; - if (span1Len < span2Len) - { - return -1; - } - - if (span1Len > span2Len) - { - return 1; - } - } - - int result = span1.CompareTo(span2, StringComparison.InvariantCulture); - if (result != 0) - { - return result; - } - } while (pos1 < len1 && pos2 < len2); - - return len1 - len2; - } - - /// <inheritdoc /> - public int Compare(string? x, string? y) - { - return CompareValues(x, y); - } - } -} diff --git a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs b/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs deleted file mode 100644 index 105e2a52ae..0000000000 --- a/tests/Jellyfin.Extensions.Tests/AlphanumericComparatorTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Linq; -using Xunit; - -namespace Jellyfin.Extensions.Tests -{ - public class AlphanumericComparatorTests - { - // InlineData is pre-sorted - [Theory] - [InlineData(null, "", "1", "9", "10", "a", "z")] - [InlineData("50F", "100F", "SR9", "SR100")] - [InlineData("image-1.jpg", "image-02.jpg", "image-4.jpg", "image-9.jpg", "image-10.jpg", "image-11.jpg", "image-22.jpg")] - [InlineData("Hard drive 2GB", "Hard drive 20GB")] - [InlineData("b", "e", "è", "ě", "f", "g", "k")] - [InlineData("123456789", "123456789a", "abc", "abcd")] - [InlineData("12345678912345678912345678913234567891", "123456789123456789123456789132345678912")] - [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567891")] - [InlineData("12345678912345678912345678913234567891", "12345678912345678912345678913234567892")] - [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891a")] - [InlineData("12345678912345678912345678913234567891a", "12345678912345678912345678913234567891b")] - [InlineData("a5", "a11")] - [InlineData("a05a", "a5b")] - [InlineData("a5a", "a05b")] - [InlineData("6xxx", "007asdf")] - [InlineData("00042Q", "42s")] - public void AlphanumericComparatorTest(params string?[] strings) - { - var copy = strings.Reverse().ToArray(); - Array.Sort(copy, new AlphanumericComparator()); - Assert.Equal(strings, copy); - } - } -} From 140c459ac3f352fd53a3cbd81410aedb0d9fe854 Mon Sep 17 00:00:00 2001 From: Richard Torhan <richard.torhan@windowslive.com> Date: Thu, 27 Nov 2025 18:00:27 +0100 Subject: [PATCH 149/390] Fix logger CA2024: Do not use StreamReader.EndOfStream in async methods --- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index 3d288b9f86..c438dbf0fc 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -27,10 +27,9 @@ namespace MediaBrowser.Controller.MediaEncoding using (target) using (reader) { - while (!reader.EndOfStream && reader.BaseStream.CanRead) + string? line = await reader.ReadLineAsync().ConfigureAwait(false); + while (line is not null && reader.BaseStream.CanRead) { - var line = await reader.ReadLineAsync().ConfigureAwait(false); - ParseLogLine(line, state); var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); @@ -50,6 +49,7 @@ namespace MediaBrowser.Controller.MediaEncoding } await target.FlushAsync().ConfigureAwait(false); + line = await reader.ReadLineAsync().ConfigureAwait(false); } } } From e70355fbe1039290bd03265c319e3ba1c75e8402 Mon Sep 17 00:00:00 2001 From: Richard Torhan <richard.torhan@windowslive.com> Date: Thu, 27 Nov 2025 18:03:10 +0100 Subject: [PATCH 150/390] Fix nullable annotation --- MediaBrowser.Controller/MediaEncoding/JobLogger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index c438dbf0fc..2702e3bc09 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -27,7 +27,7 @@ namespace MediaBrowser.Controller.MediaEncoding using (target) using (reader) { - string? line = await reader.ReadLineAsync().ConfigureAwait(false); + string line = await reader.ReadLineAsync().ConfigureAwait(false); while (line is not null && reader.BaseStream.CanRead) { ParseLogLine(line, state); From d089537bca2a6998cd132122901fcf198d67d89e Mon Sep 17 00:00:00 2001 From: Richard Torhan <richard.torhan@windowslive.com> Date: Thu, 27 Nov 2025 18:07:07 +0100 Subject: [PATCH 151/390] Fix error CA1849: 'ZipFile.ExtractToDirectory(Stream, string, bool)' synchronously blocks. Await 'ZipFile.ExtractToDirectoryAsync(Stream, string, bool, CancellationToken)' instead. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1849) --- Emby.Server.Implementations/Updates/InstallationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 5f9e29b563..6e85cfd774 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -562,7 +562,7 @@ namespace Emby.Server.Implementations.Updates } stream.Position = 0; - ZipFile.ExtractToDirectory(stream, targetDir, true); + await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true); // Ensure we create one or populate existing ones with missing data. await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); From 50dcec1ff5b2f3e5af83536aa390809e8bcae9ab Mon Sep 17 00:00:00 2001 From: Richard Torhan <richard.torhan@windowslive.com> Date: Thu, 27 Nov 2025 18:11:05 +0100 Subject: [PATCH 152/390] Fix error CA2016: Forward the 'cancellationToken' parameter to the 'ExtractToDirectoryAsync' method or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token --- Emby.Server.Implementations/Updates/InstallationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6e85cfd774..67b77a112d 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -562,7 +562,7 @@ namespace Emby.Server.Implementations.Updates } stream.Position = 0; - await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true); + await ZipFile.ExtractToDirectoryAsync(stream, targetDir, true, cancellationToken); // Ensure we create one or populate existing ones with missing data. await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); From 89b5b99873cc8f8a3a26bac1102232cc879bac82 Mon Sep 17 00:00:00 2001 From: Richard Torhan <richard.torhan@windowslive.com> Date: Thu, 27 Nov 2025 18:16:47 +0100 Subject: [PATCH 153/390] Update Actions to .NET 10 --- .github/workflows/ci-codeql-analysis.yml | 2 +- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 6 ++---- .github/workflows/ci-tests.yml | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 6d4f4edb69..a0ddf643a2 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Initialize CodeQL uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 2ca101591f..7c1ca6c06c 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Build run: | @@ -49,7 +49,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: - dotnet-version: '9.0.x' + dotnet-version: '10.0.x' - name: Checkout common ancestor env: diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index cf2a2868d5..b4ecd8bec9 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -24,8 +24,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: - dotnet-version: '9.0.x' - + 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" @@ -62,8 +61,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: - dotnet-version: '9.0.x' - + 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" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index f70243221d..456fce787f 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -9,7 +9,7 @@ on: pull_request: env: - SDK_VERSION: "9.0.x" + SDK_VERSION: "10.0.x" jobs: run-tests: From e12131108e2cbe9df2155383e870f0f3dc65a6b5 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 14 Jan 2026 18:59:02 +0100 Subject: [PATCH 154/390] Update to 10.0.2 --- Directory.Packages.props | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d78e2d021d..b13fabeeab 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,27 +26,27 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> @@ -58,10 +58,10 @@ <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" /> <PackageVersion Include="Polly" Version="8.6.5" /> - <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" /> + <PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> - <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" /> + <PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" /> @@ -77,7 +77,7 @@ <PackageVersion Include="Svg.Skia" Version="3.2.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> - <PackageVersion Include="System.Text.Json" Version="10.0.0" /> + <PackageVersion Include="System.Text.Json" Version="10.0.2" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.10.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> From 6a9bb060ebb7a5e9d4a9a016403f0fad0ad63218 Mon Sep 17 00:00:00 2001 From: rimasx <riks_12@hot.ee> Date: Thu, 15 Jan 2026 09:18:56 -0500 Subject: [PATCH 155/390] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index 2e692009bf..91a0aa6639 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -72,7 +72,7 @@ "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval", "NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.", "NameSeasonUnknown": "Tundmatu hooaeg", - "NameSeasonNumber": "Hooaeg {0}", + "NameSeasonNumber": "{0}. hooaeg", "NameInstallFailed": "{0} paigaldamine nurjus", "MusicVideos": "Muusikavideod", "Music": "Muusika", From ef0409d06c0ace3ef938100e1facca7e59121713 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:09:41 +0000 Subject: [PATCH 156/390] Update dependency Diacritics to 4.1.4 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b13fabeeab..41c7adeca2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="6.0.4" /> - <PackageVersion Include="Diacritics" Version="4.0.17" /> + <PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="FsCheck.Xunit" Version="3.3.2" /> From 8d052a6cb17421e6c7774bafc6957029f3c9d4ae Mon Sep 17 00:00:00 2001 From: Abitofevrything <54505189+abitofevrything@users.noreply.github.com> Date: Sun, 18 Jan 2026 12:17:06 +0100 Subject: [PATCH 157/390] Merge pull request #15926 from abitofevrything/feat/accurate_hls_seeking Refactor HLS transcode seeking --- Jellyfin.Api/Controllers/AudioController.cs | 6 ---- .../Controllers/DynamicHlsController.cs | 36 ------------------- .../Controllers/UniversalAudioController.cs | 8 +---- Jellyfin.Api/Controllers/VideosController.cs | 6 ---- Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +- .../MediaEncoding/BaseEncodingJobOptions.cs | 2 -- .../MediaEncoding/EncodingHelper.cs | 35 ++++++++++-------- .../MediaEncoding/EncodingJobInfo.cs | 15 -------- .../Transcoding/TranscodeManager.cs | 2 +- .../Configuration/EncodingOptions.cs | 8 +++++ .../Configuration/HlsAudioSeekStrategy.cs | 23 ++++++++++++ MediaBrowser.Model/Dlna/StreamBuilder.cs | 1 - MediaBrowser.Model/Dlna/StreamInfo.cs | 8 ----- MediaBrowser.Model/Dlna/TranscodingProfile.cs | 4 +-- .../Dlna/LegacyStreamInfo.cs | 2 -- .../Test Data/DeviceProfile-AndroidPixel.json | 3 -- .../DeviceProfile-AndroidTVExoPlayer.json | 2 -- .../Test Data/DeviceProfile-Chrome-NoHLS.json | 11 ------ .../Test Data/DeviceProfile-Chrome.json | 7 ++-- .../Test Data/DeviceProfile-Firefox.json | 3 -- .../DeviceProfile-JellyfinMediaPlayer.json | 3 -- .../Test Data/DeviceProfile-LowBandwidth.json | 3 -- .../Test Data/DeviceProfile-RokuSSPlus.json | 6 ---- .../DeviceProfile-RokuSSPlusNext.json | 6 ---- .../Test Data/DeviceProfile-SafariNext.json | 7 ++-- .../DeviceProfile-Tizen3-stereo.json | 13 ------- .../DeviceProfile-Tizen4-4K-5.1.json | 13 ------- .../DeviceProfile-TranscodeMedia.json | 4 --- .../Test Data/DeviceProfile-WebOS-23.json | 4 +-- .../Test Data/DeviceProfile-Yatse.json | 3 -- .../Test Data/DeviceProfile-Yatse2.json | 3 -- 31 files changed, 61 insertions(+), 188 deletions(-) create mode 100644 MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index e334e12640..4be79ff5a0 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -50,7 +50,6 @@ public class AudioController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -107,7 +106,6 @@ public class AudioController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -159,7 +157,6 @@ public class AudioController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -217,7 +214,6 @@ public class AudioController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -274,7 +270,6 @@ public class AudioController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -326,7 +321,6 @@ public class AudioController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 1e3e2740f0..15b04051f4 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -122,7 +122,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -182,7 +181,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -238,7 +236,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -364,7 +361,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -425,7 +421,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -481,7 +476,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -543,7 +537,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> @@ -601,7 +594,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxStreamingBitrate, @@ -654,7 +646,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate ?? maxStreamingBitrate, @@ -713,7 +704,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -771,7 +761,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -826,7 +815,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -887,7 +875,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> @@ -943,7 +930,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxStreamingBitrate, @@ -996,7 +982,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate ?? maxStreamingBitrate, @@ -1060,7 +1045,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -1124,7 +1108,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -1181,7 +1164,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -1247,7 +1229,6 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param> @@ -1309,7 +1290,6 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxStreamingBitrate, @@ -1364,7 +1344,6 @@ public class DynamicHlsController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate ?? maxStreamingBitrate, @@ -1586,16 +1565,6 @@ public class DynamicHlsController : BaseJellyfinApiController var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); - if (state.BaseRequest.BreakOnNonKeyFrames) - { - // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe - // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable - // to produce a missing part of video stream before first keyframe is encountered, which may lead to - // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js - _logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request"); - state.BaseRequest.BreakOnNonKeyFrames = false; - } - var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)); @@ -1746,11 +1715,6 @@ public class DynamicHlsController : BaseJellyfinApiController var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs; - if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec)) - { - return copyArgs + " -copypriorss:a:0 0"; - } - return copyArgs; } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index fd63347030..b1a91ae70f 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -83,7 +83,6 @@ public class UniversalAudioController : BaseJellyfinApiController /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param> /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param> /// <response code="200">Audio stream returned.</response> /// <response code="302">Redirected to remote audio stream.</response> @@ -114,7 +113,6 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] int? maxAudioBitDepth, [FromQuery] bool? enableRemoteMedia, [FromQuery] bool enableAudioVbrEncoding = true, - [FromQuery] bool breakOnNonKeyFrames = false, [FromQuery] bool enableRedirection = true) { userId = RequestHelpers.GetUserId(User, userId); @@ -127,7 +125,7 @@ public class UniversalAudioController : BaseJellyfinApiController return NotFound(); } - var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); + var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); @@ -208,7 +206,6 @@ public class UniversalAudioController : BaseJellyfinApiController EnableAutoStreamCopy = true, AllowAudioStreamCopy = true, AllowVideoStreamCopy = true, - BreakOnNonKeyFrames = breakOnNonKeyFrames, AudioSampleRate = maxAudioSampleRate, MaxAudioChannels = maxAudioChannels, MaxAudioBitDepth = maxAudioBitDepth, @@ -242,7 +239,6 @@ public class UniversalAudioController : BaseJellyfinApiController EnableAutoStreamCopy = true, AllowAudioStreamCopy = true, AllowVideoStreamCopy = true, - BreakOnNonKeyFrames = breakOnNonKeyFrames, AudioSampleRate = maxAudioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate), @@ -263,7 +259,6 @@ public class UniversalAudioController : BaseJellyfinApiController string? transcodingContainer, string? audioCodec, MediaStreamProtocol? transcodingProtocol, - bool? breakOnNonKeyFrames, int? transcodingAudioChannels, int? maxAudioSampleRate, int? maxAudioBitDepth, @@ -298,7 +293,6 @@ public class UniversalAudioController : BaseJellyfinApiController Container = transcodingContainer ?? "mp3", AudioCodec = audioCodec ?? "mp3", Protocol = transcodingProtocol ?? MediaStreamProtocol.http, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } }; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index e7c6f23ce5..ccf8e90632 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -270,7 +270,6 @@ public class VideosController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -329,7 +328,6 @@ public class VideosController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -386,7 +384,6 @@ public class VideosController : BaseJellyfinApiController EnableAutoStreamCopy = enableAutoStreamCopy ?? true, AllowAudioStreamCopy = allowAudioStreamCopy ?? true, AllowVideoStreamCopy = allowVideoStreamCopy ?? true, - BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, AudioSampleRate = audioSampleRate, MaxAudioChannels = maxAudioChannels, AudioBitRate = audioBitRate, @@ -511,7 +508,6 @@ public class VideosController : BaseJellyfinApiController /// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param> /// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param> /// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param> - /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param> /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param> @@ -570,7 +566,6 @@ public class VideosController : BaseJellyfinApiController [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, - [FromQuery] bool? breakOnNonKeyFrames, [FromQuery] int? audioSampleRate, [FromQuery] int? maxAudioBitDepth, [FromQuery] int? audioBitRate, @@ -624,7 +619,6 @@ public class VideosController : BaseJellyfinApiController enableAutoStreamCopy, allowVideoStreamCopy, allowAudioStreamCopy, - breakOnNonKeyFrames, audioSampleRate, maxAudioBitDepth, audioBitRate, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index b3f5b9a801..1e984542ec 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -201,7 +201,7 @@ public static class StreamingHelpers state.OutputVideoCodec = state.Request.VideoCodec; state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec); - encodingHelper.TryStreamCopy(state); + encodingHelper.TryStreamCopy(state, encodingOptions); if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue) { diff --git a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs index 20f51ddb71..10f2f04af6 100644 --- a/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/BaseEncodingJobOptions.cs @@ -43,8 +43,6 @@ namespace MediaBrowser.Controller.MediaEncoding public bool AllowAudioStreamCopy { get; set; } - public bool BreakOnNonKeyFrames { get; set; } - /// <summary> /// Gets or sets the audio sample rate. /// </summary> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 91d88dc08b..11eee1a372 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2914,8 +2914,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (time > 0) { - // For direct streaming/remuxing, we seek at the exact position of the keyframe - // However, ffmpeg will seek to previous keyframe when the exact time is the input + // For direct streaming/remuxing, HLS segments start at keyframes. + // However, ffmpeg will seek to previous keyframe when the exact frame time is the input // Workaround this by adding 0.5s offset to the seeking time to get the exact keyframe on most videos. // This will help subtitle syncing. var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec); @@ -2932,17 +2932,16 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.IsVideoRequest) { - var outputVideoCodec = GetVideoEncoder(state, options); - var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); - - // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking - // Disable -noaccurate_seek on mpegts container due to the timestamps issue on some clients, - // but it's still required for fMP4 container otherwise the audio can't be synced to the video. - if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) - && !string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase) - && state.TranscodingType != TranscodingJobType.Progressive - && !state.EnableBreakOnNonKeyFrames(outputVideoCodec) - && (state.BaseRequest.StartTimeTicks ?? 0) > 0) + // If we are remuxing, then the copied stream cannot be seeked accurately (it will seek to the nearest + // keyframe). If we are using fMP4, then force all other streams to use the same inaccurate seeking to + // avoid A/V sync issues which cause playback issues on some devices. + // When remuxing video, the segment start times correspond to key frames in the source stream, so this + // option shouldn't change the seeked point that much. + // Important: make sure not to use it with wtv because it breaks seeking + if (state.TranscodingType is TranscodingJobType.Hls + && string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase) + && (IsCopyCodec(state.OutputVideoCodec) || IsCopyCodec(state.OutputAudioCodec)) + && !string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)) { seekParam += " -noaccurate_seek"; } @@ -7084,7 +7083,7 @@ namespace MediaBrowser.Controller.MediaEncoding } #nullable disable - public void TryStreamCopy(EncodingJobInfo state) + public void TryStreamCopy(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream is not null && CanStreamCopyVideo(state, state.VideoStream)) { @@ -7101,8 +7100,14 @@ namespace MediaBrowser.Controller.MediaEncoding } } + var preventHlsAudioCopy = state.TranscodingType is TranscodingJobType.Hls + && state.VideoStream is not null + && !IsCopyCodec(state.OutputVideoCodec) + && options.HlsAudioSeekStrategy is HlsAudioSeekStrategy.TranscodeAudio; + if (state.AudioStream is not null - && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) + && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs) + && !preventHlsAudioCopy) { state.OutputAudioCodec = "copy"; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 43680f5c01..7d0384ef27 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -515,21 +515,6 @@ namespace MediaBrowser.Controller.MediaEncoding public int HlsListSize => 0; - public bool EnableBreakOnNonKeyFrames(string videoCodec) - { - if (TranscodingType != TranscodingJobType.Progressive) - { - if (IsSegmentedLiveStream) - { - return false; - } - - return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec); - } - - return false; - } - private int? GetMediaStreamCount(MediaStreamType type, int limit) { var count = MediaSource.GetStreamCount(type); diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 2fd054f110..defd855ec0 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -673,7 +673,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable if (state.VideoRequest is not null) { - _encodingHelper.TryStreamCopy(state); + _encodingHelper.TryStreamCopy(state, encodingOptions); } } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index f7f386d289..98fc2e632f 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -1,6 +1,7 @@ #pragma warning disable CA1819 // XML serialization handles collections improperly, so we need to use arrays #nullable disable +using System.ComponentModel; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Configuration; @@ -60,6 +61,7 @@ public class EncodingOptions SubtitleExtractionTimeoutMinutes = 30; AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"]; HardwareDecodingCodecs = ["h264", "vc1"]; + HlsAudioSeekStrategy = HlsAudioSeekStrategy.DisableAccurateSeek; } /// <summary> @@ -301,4 +303,10 @@ public class EncodingOptions /// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for. /// </summary> public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; } + + /// <summary> + /// Gets or sets the method used for audio seeking in HLS. + /// </summary> + [DefaultValue(HlsAudioSeekStrategy.DisableAccurateSeek)] + public HlsAudioSeekStrategy HlsAudioSeekStrategy { get; set; } } diff --git a/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs b/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs new file mode 100644 index 0000000000..49feeb435f --- /dev/null +++ b/MediaBrowser.Model/Configuration/HlsAudioSeekStrategy.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.Configuration +{ + /// <summary> + /// An enum representing the options to seek the input audio stream when + /// transcoding HLS segments. + /// </summary> + public enum HlsAudioSeekStrategy + { + /// <summary> + /// If the video stream is transcoded and the audio stream is copied, + /// seek the video stream to the same keyframe as the audio stream. The + /// resulting timestamps in the output streams may be inaccurate. + /// </summary> + DisableAccurateSeek = 0, + + /// <summary> + /// Prevent audio streams from being copied if the video stream is transcoded. + /// The resulting timestamps will be accurate, but additional audio transcoding + /// overhead will be incurred. + /// </summary> + TranscodeAudio = 1, + } +} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 61e04a8134..42cb208d08 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -610,7 +610,6 @@ namespace MediaBrowser.Model.Dlna playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest; playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode; - playlistItem.BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames; playlistItem.EnableAudioVbrEncoding = transcodingProfile.EnableAudioVbrEncoding; if (transcodingProfile.MinSegments > 0) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 92404de508..9edb4115cc 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -86,11 +86,6 @@ public class StreamInfo /// <value>The minimum segments count.</value> public int? MinSegments { get; set; } - /// <summary> - /// Gets or sets a value indicating whether the stream can be broken on non-keyframes. - /// </summary> - public bool BreakOnNonKeyFrames { get; set; } - /// <summary> /// Gets or sets a value indicating whether the stream requires AVC. /// </summary> @@ -1018,9 +1013,6 @@ public class StreamInfo sb.Append("&MinSegments="); sb.Append(MinSegments.Value.ToString(CultureInfo.InvariantCulture)); } - - sb.Append("&BreakOnNonKeyFrames="); - sb.Append(BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)); } else { diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index 5797d42506..f49b24976a 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -41,7 +41,6 @@ public class TranscodingProfile MaxAudioChannels = other.MaxAudioChannels; MinSegments = other.MinSegments; SegmentLength = other.SegmentLength; - BreakOnNonKeyFrames = other.BreakOnNonKeyFrames; Conditions = other.Conditions; EnableAudioVbrEncoding = other.EnableAudioVbrEncoding; } @@ -143,7 +142,8 @@ public class TranscodingProfile /// </summary> [DefaultValue(false)] [XmlAttribute("breakOnNonKeyFrames")] - public bool BreakOnNonKeyFrames { get; set; } + [Obsolete("This is always false")] + public bool? BreakOnNonKeyFrames { get; set; } /// <summary> /// Gets or sets the profile conditions. diff --git a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs index e32baef55d..6436d7d0e4 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs @@ -134,8 +134,6 @@ public class LegacyStreamInfo : StreamInfo { list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); } - - list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); } else { diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json index 68ce3ea4ab..643ff2638c 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidPixel.json @@ -152,7 +152,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -169,7 +168,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -185,7 +183,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json index 3d3968268f..44f63f384f 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-AndroidTVExoPlayer.json @@ -130,7 +130,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -146,7 +145,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json index 5d1f5f1620..f1fc9e0dbe 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome-NoHLS.json @@ -127,7 +127,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -144,7 +143,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -161,7 +159,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -178,7 +175,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -195,7 +191,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -212,7 +207,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -229,7 +223,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -246,7 +239,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -263,7 +255,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -281,7 +272,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -298,7 +288,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json index e2f75b569b..7e37a6236e 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Chrome.json @@ -107,7 +107,6 @@ "Protocol": "hls", "MaxAudioChannels": "2", "MinSegments": "2", - "BreakOnNonKeyFrames": true, "EnableAudioVbrEncoding": true }, { @@ -182,8 +181,7 @@ "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", - "MinSegments": "2", - "BreakOnNonKeyFrames": true + "MinSegments": "2" }, { "Container": "ts", @@ -193,8 +191,7 @@ "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", - "MinSegments": "2", - "BreakOnNonKeyFrames": true + "MinSegments": "2" } ], "ContainerProfiles": [], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json index 21ae7e5cb3..4380d80efa 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Firefox.json @@ -95,7 +95,6 @@ "TranscodingProfiles": [ { "AudioCodec": "aac", - "BreakOnNonKeyFrames": true, "Container": "mp4", "Context": "Streaming", "EnableAudioVbrEncoding": true, @@ -170,7 +169,6 @@ }, { "AudioCodec": "aac,mp2,opus,flac", - "BreakOnNonKeyFrames": true, "Container": "mp4", "Context": "Streaming", "MaxAudioChannels": "2", @@ -181,7 +179,6 @@ }, { "AudioCodec": "aac,mp3,mp2", - "BreakOnNonKeyFrames": true, "Container": "ts", "Context": "Streaming", "MaxAudioChannels": "2", diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json index da9a1a4ada..cca1c16ee7 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-JellyfinMediaPlayer.json @@ -30,7 +30,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -48,7 +47,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -62,7 +60,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json index 82b73fb0f8..b7cd170b9f 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-LowBandwidth.json @@ -30,7 +30,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -48,7 +47,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -62,7 +60,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json index 37b923558b..b823ac4b85 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlus.json @@ -49,7 +49,6 @@ "MaxAudioChannels": " 2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -66,7 +65,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -83,7 +81,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -100,7 +97,6 @@ "MaxAudioChannels": " 2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -118,7 +114,6 @@ "MaxAudioChannels": " 2", "MinSegments": 1, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -135,7 +130,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json index 542bf6370a..708ff73c4d 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-RokuSSPlusNext.json @@ -49,7 +49,6 @@ "MaxAudioChannels": " 2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -66,7 +65,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -83,7 +81,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -100,7 +97,6 @@ "MaxAudioChannels": " 2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -118,7 +114,6 @@ "MaxAudioChannels": " 2", "MinSegments": 1, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -135,7 +130,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json index f61d0e36bd..10382fa82e 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-SafariNext.json @@ -114,7 +114,6 @@ "Protocol": "hls", "MaxAudioChannels": "6", "MinSegments": "2", - "BreakOnNonKeyFrames": true, "EnableAudioVbrEncoding": true }, { @@ -173,8 +172,7 @@ "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", - "MinSegments": "2", - "BreakOnNonKeyFrames": true + "MinSegments": "2" }, { "Container": "ts", @@ -184,8 +182,7 @@ "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "2", - "MinSegments": "2", - "BreakOnNonKeyFrames": true + "MinSegments": "2" } ], "ContainerProfiles": [], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json index 9d43d2166d..3625b099c1 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json @@ -165,7 +165,6 @@ "MaxAudioChannels": "2", "MinSegments": 1, "SegmentLength": 0, - "BreakOnNonKeyFrames": true, "$type": "TranscodingProfile" }, { @@ -182,7 +181,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -199,7 +197,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -216,7 +213,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -233,7 +229,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -250,7 +245,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -267,7 +261,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -284,7 +277,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -301,7 +293,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -319,7 +310,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", @@ -346,7 +336,6 @@ "MaxAudioChannels": "2", "MinSegments": 1, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", @@ -373,7 +362,6 @@ "MaxAudioChannels": "2", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", @@ -399,7 +387,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json index 3859ef9941..deee650b29 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json @@ -165,7 +165,6 @@ "MaxAudioChannels": "6", "MinSegments": 1, "SegmentLength": 0, - "BreakOnNonKeyFrames": true, "$type": "TranscodingProfile" }, { @@ -182,7 +181,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -199,7 +197,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -216,7 +213,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -233,7 +229,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -250,7 +245,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -267,7 +261,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -284,7 +277,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -301,7 +293,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -319,7 +310,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", @@ -346,7 +336,6 @@ "MaxAudioChannels": "6", "MinSegments": 1, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", @@ -373,7 +362,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", @@ -399,7 +387,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "Conditions": [ { "Condition": "LessThanEqual", diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json index 9fc1ae6bb2..38de51b045 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-TranscodeMedia.json @@ -16,7 +16,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -28,7 +27,6 @@ "Protocol": "hls", "MaxAudioChannels": "2", "MinSegments": "2", - "BreakOnNonKeyFrames": true, "$type": "TranscodingProfile" }, { @@ -40,7 +38,6 @@ "Protocol": "hls", "MaxAudioChannels": "2", "MinSegments": "2", - "BreakOnNonKeyFrames": true, "$type": "TranscodingProfile" }, { @@ -64,7 +61,6 @@ "EnableSubtitlesInManifest": false, "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json index 094b0723b1..3ff11a684f 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-WebOS-23.json @@ -135,7 +135,6 @@ "Protocol": "hls", "MaxAudioChannels": "6", "MinSegments": "1", - "BreakOnNonKeyFrames": false, "EnableAudioVbrEncoding": true }, { @@ -210,8 +209,7 @@ "Context": "Streaming", "Protocol": "hls", "MaxAudioChannels": "6", - "MinSegments": "1", - "BreakOnNonKeyFrames": false + "MinSegments": "1" } ], "ContainerProfiles": [], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json index 256c8dc2f0..838a1f920c 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse.json @@ -52,7 +52,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -70,7 +69,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -88,7 +86,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json index 256c8dc2f0..838a1f920c 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Yatse2.json @@ -52,7 +52,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -70,7 +69,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" }, { @@ -88,7 +86,6 @@ "MaxAudioChannels": "6", "MinSegments": 0, "SegmentLength": 0, - "BreakOnNonKeyFrames": false, "$type": "TranscodingProfile" } ], From 84ebed1eb7edc7bdad6513ae0492c32f45f91938 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 07:40:09 -0700 Subject: [PATCH 158/390] Update Microsoft to v5 (#15486) --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b13fabeeab..2bafece64c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,8 +29,8 @@ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> - <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" /> - <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" /> + <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" /> + <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" /> From aa666565d1bb91c01762417dc035c237176b80d2 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:36 -0500 Subject: [PATCH 159/390] Backport pull request #15808 from jellyfin/release-10.11.z Trim music artist names Original-merge: 093cfc3f3b72a6bea71cb96ced180a9ac257d537 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 28ea2033d8..605d2aeec2 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -421,7 +421,7 @@ public class ItemUpdateController : BaseJellyfinApiController { if (item is IHasAlbumArtist hasAlbumArtists) { - hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name); + hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim()); } } @@ -429,7 +429,7 @@ public class ItemUpdateController : BaseJellyfinApiController { if (item is IHasArtist hasArtists) { - hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name); + hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim()); } } From f9fd34b11ec962d8d76bf36854c4d4e7e037bdc5 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:37 -0500 Subject: [PATCH 160/390] Backport pull request #15872 from jellyfin/release-10.11.z Be more strict about PersonType assignment Original-merge: b56de6493f67cd1cdc43b47745ae66908d1aef41 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 4 +--- MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs | 7 ++++--- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 414a0a3c9b..2beb34e43b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -303,9 +303,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index e30c555cb4..f0e159f098 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 1b429039e7..0905a3bdcb 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -120,9 +120,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index f0828e8263..82d4e58384 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -367,9 +367,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index f5e59a2789..d6e66a0e61 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -70,18 +70,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public static PersonKind MapCrewToPersonType(Crew crew) { if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) - && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase)) + && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Director; } if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) - && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase)) + && crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Producer; } - if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)) + if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase) + && crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Writer; } From 09edca8b7a9174c374a7d03bb1ec3aea32d02ffd Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:38 -0500 Subject: [PATCH 161/390] Backport pull request #15899 from jellyfin/release-10.11.z Fix watched state not kept on Media replace/rename Original-merge: 8433b6d8a41f66f6eef36bb950927c6a6afa1a36 Merged-by: joshuaboniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com> --- CONTRIBUTORS.md | 1 + .../Library/LibraryManager.cs | 6 +++ .../Item/BaseItemRepository.cs | 37 ++++++++++++------- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++ .../Library/ILibraryManager.cs | 8 ++++ .../Persistence/IItemRepository.cs | 8 ++++ .../Manager/MetadataService.cs | 11 ++++-- 7 files changed, 57 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 171509382d..1770db60be 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -210,6 +210,7 @@ - [bjorntp](https://github.com/bjorntp) - [martenumberto](https://github.com/martenumberto) - [ZeusCraft10](https://github.com/ZeusCraft10) + - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) # Emby Contributors diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f35d85f659..bdf04edc2e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync([item], parent, updateReason, cancellationToken); + /// <inheritdoc /> + public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken) + { + await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false); + } + public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { if (item.IsFileProtocol) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 3b3d3c4f43..646a9c4836 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -624,7 +624,6 @@ public sealed class BaseItemRepository var ids = tuples.Select(f => f.Item.Id).ToArray(); var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); - var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray(); foreach (var item in tuples) { @@ -658,19 +657,6 @@ public sealed class BaseItemRepository context.SaveChanges(); - foreach (var item in newItems) - { - // reattach old userData entries - var userKeys = item.UserDataKey.ToArray(); - var retentionDate = (DateTime?)null; - context.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdate(e => e - .SetProperty(f => f.ItemId, item.Item.Id) - .SetProperty(f => f.RetentionDate, retentionDate)); - } - var itemValueMaps = tuples .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) .ToArray(); @@ -766,6 +752,29 @@ public sealed class BaseItemRepository transaction.Commit(); } + /// <inheritdoc /> + public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(item); + cancellationToken.ThrowIfCancellationRequested(); + + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + await using (dbContext.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + } + } + /// <inheritdoc /> public BaseItemDto? RetrieveItem(Guid id) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d9d2d0e3a8..7586b99e77 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); + public async Task ReattachUserDataAsync(CancellationToken cancellationToken) => + await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false); + /// <summary> /// Validates that images within the item are still on the filesystem. /// </summary> diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index fcc5ed672a..675812ac23 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library /// <returns>Returns a Task that can be awaited.</returns> Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// <summary> + /// Reattaches the user data to the item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A task that represents the asynchronous reattachment operation.</returns> + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// <summary> /// Retrieves the item. /// </summary> diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 00c492742a..bf80b7d0a8 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -35,6 +35,14 @@ public interface IItemRepository Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); + /// <summary> + /// Reattaches the user data to the item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>A task that represents the asynchronous reattachment operation.</returns> + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// <summary> /// Retrieves the item. /// </summary> diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index a2102ca9cd..e9cb46eab5 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager if (isFirstRefresh) { - await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false); } // Next run metadata providers @@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false); } return updateType; @@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager } } - protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken) { await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); + if (reattachUserData) + { + await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); + } + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; From afcaec0a894df038f8b88a517a01bace0d3c237c Mon Sep 17 00:00:00 2001 From: Collin-Swish <79892877+Collin-Swish@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:39 -0500 Subject: [PATCH 162/390] Backport pull request #15965 from jellyfin/release-10.11.z Add mblink creation logic to library update endpoint. Original-merge: 22d593b8e986ecdb42fb1e618bfcf833b0a6f118 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Library/LibraryManager.cs | 33 +++++++++++-------- .../Controllers/LibraryStructureController.cs | 11 +++++++ .../Library/ILibraryManager.cs | 7 ++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bdf04edc2e..f7f5c387e1 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3201,19 +3201,7 @@ namespace Emby.Server.Implementations.Library var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); - var shortcutFilename = Path.GetFileNameWithoutExtension(path); - - var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - - while (File.Exists(lnk)) - { - shortcutFilename += "1"; - lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - } - - _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); - - RemoveContentTypeOverrides(path); + CreateShortcut(virtualFolderPath, pathInfo); if (saveLibraryOptions) { @@ -3378,5 +3366,24 @@ namespace Emby.Server.Implementations.Library return item is UserRootFolder || item.IsVisibleStandalone(user); } + + public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo) + { + var path = pathInfo.Path; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; + + var shortcutFilename = Path.GetFileNameWithoutExtension(path); + + var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); + + while (File.Exists(lnk)) + { + shortcutFilename += "1"; + lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); + } + + _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); + RemoveContentTypeOverrides(path); + } } } diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 2a885662b5..117811429a 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController return NotFound(); } + LibraryOptions options = item.GetLibraryOptions(); + foreach (var mediaPath in request.LibraryOptions!.PathInfos) + { + if (options.PathInfos.Any(i => i.Path == mediaPath.Path)) + { + continue; + } + + _libraryManager.CreateShortcut(item.Path, mediaPath); + } + item.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 675812ac23..df1c98f3f7 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -660,5 +660,12 @@ namespace MediaBrowser.Controller.Library /// This exists so plugins can trigger a library scan. /// </remarks> void QueueLibraryScan(); + + /// <summary> + /// Add mblink file for a media path. + /// </summary> + /// <param name="virtualFolderPath">The path to the virtualfolder.</param> + /// <param name="pathInfo">The new virtualfolder.</param> + public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo); } } From c4ffc357a3d3658526f6fd879364145333eea6b0 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:41 -0500 Subject: [PATCH 163/390] Backport pull request #15983 from jellyfin/release-10.11.z Prioritize better matches on search Original-merge: a518160a6ff471541b7daae6d54c8b896bb1f2e6 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 29 ++++++++++++------- .../Item/OrderMapper.cs | 27 +++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 646a9c4836..a2f0e78c03 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1615,29 +1615,36 @@ public sealed class BaseItemRepository IOrderedQueryable<BaseItemEntity>? orderedQuery = null; + // When searching, prioritize by match quality: exact match > prefix match > contains + if (hasSearch) + { + orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!)); + } + var firstOrdering = orderBy.FirstOrDefault(); if (firstOrdering != default) { var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context); - if (firstOrdering.SortOrder == SortOrder.Ascending) + if (orderedQuery is null) { - orderedQuery = query.OrderBy(expression); + // No search relevance ordering, start fresh + orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending + ? query.OrderBy(expression) + : query.OrderByDescending(expression); } else { - orderedQuery = query.OrderByDescending(expression); + // Search relevance ordering already applied, chain with ThenBy + orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending + ? orderedQuery.ThenBy(expression) + : orderedQuery.ThenByDescending(expression); } if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName) { - if (firstOrdering.SortOrder is SortOrder.Ascending) - { - orderedQuery = orderedQuery.ThenBy(e => e.Name); - } - else - { - orderedQuery = orderedQuery.ThenByDescending(e => e.Name); - } + orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending + ? orderedQuery.ThenBy(e => e.Name) + : orderedQuery.ThenByDescending(e => e.Name); } } diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index 192ee74996..1ae7cc6c4a 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using Microsoft.EntityFrameworkCore; @@ -68,4 +69,30 @@ public static class OrderMapper _ => e => e.SortName }; } + + /// <summary> + /// Creates an expression to order search results by match quality. + /// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3). + /// </summary> + /// <param name="searchTerm">The search term to match against.</param> + /// <returns>An expression that returns an integer representing match quality (lower is better).</returns> + public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm) + { + var cleanSearchTerm = GetCleanValue(searchTerm); + var searchPrefix = cleanSearchTerm + " "; + return e => + e.CleanName == cleanSearchTerm ? 0 : + e.CleanName!.StartsWith(searchPrefix) ? 1 : + e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3; + } + + private static string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + return value.RemoveDiacritics().ToLowerInvariant(); + } } From 42ff2533395fafffb7825dacbbb2db746e211ca6 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:42 -0500 Subject: [PATCH 164/390] Backport pull request #16020 from jellyfin/release-10.11.z Fix birthplace not saving correctly Original-merge: 49775b1f6aaa958f19a0ee4ea05bb9aab78c6b5b Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index a2f0e78c03..90aa3a22ee 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -882,7 +882,7 @@ public sealed class BaseItemRepository } dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray(); - dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? []; + dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? []; dto.Studios = entity.Studios?.Split('|') ?? []; dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|'); @@ -1044,7 +1044,7 @@ 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) : 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.LockedFields = dto.LockedFields is not null ? dto.LockedFields From c9b7c5bb5665d2eb02381fa12dcce786fee9ae3d Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:43 -0500 Subject: [PATCH 165/390] Backport pull request #16029 from jellyfin/release-10.11.z Skip hidden directories and .ignore paths in library monitoring Original-merge: 2cb7fb52d2221d9daa39206089b578c2c0fcb549 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 6 ++++++ Emby.Server.Implementations/Library/IgnorePatterns.cs | 1 + .../Library/IgnorePatternsTests.cs | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index d87ad729ee..7cff2a25b6 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO return; } + var fileInfo = _fileSystem.GetFileSystemInfo(path); + if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null)) + { + return; + } + // Ignore certain files, If the parent of an ignored path has a change event, ignore that too foreach (var i in _tempIgnoredPaths.Keys) { diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index fe3a1ce611..5fac2f6b0a 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", + "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 07061cfc77..4cb6cb9607 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", false)] + [InlineData("/media/.hiddendir/file.mp4", true)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } From fb32709259c0d1bb38d8ace921dffee7df4dcc39 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:44 -0500 Subject: [PATCH 166/390] Backport pull request #16046 from jellyfin/release-10.11.z Restore weekly refresh for library folder images Original-merge: 338b480217499bc37ce4bbe214aea0f1f7d9eb6a Merged-by: cvium <cvium@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Images/CollectionFolderImageProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 273d356a39..a25373326f 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex); } + + protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image) + { + var age = DateTime.UtcNow - image.DateModified; + return age.TotalDays > 7; + } } } From d1c97b8e1d58cff3dd7158718a8129a0d84112ca Mon Sep 17 00:00:00 2001 From: Vignesh Skanda <agvskanda@gmail.com> Date: Sun, 18 Jan 2026 22:52:51 +0530 Subject: [PATCH 167/390] Fix typos in XML documentation comments (#15997) * Fix typos in XML documentation comments * Update src/Jellyfin.Networking/Manager/NetworkManager.cs --------- Co-authored-by: Bond-009 <bond.009@outlook.com> --- src/Jellyfin.Networking/Manager/NetworkManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 9127606bab..a9136aad48 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -114,7 +114,7 @@ public class NetworkManager : INetworkManager, IDisposable public static string MockNetworkSettings { get; set; } = string.Empty; /// <summary> - /// Gets a value indicating whether IP4 is enabled. + /// Gets a value indicating whether IPv4 is enabled. /// </summary> public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4; @@ -361,7 +361,7 @@ public class NetworkManager : INetworkManager, IDisposable } /// <summary> - /// Filteres a list of bind addresses and exclusions on available interfaces. + /// Filters a list of bind addresses and exclusions on available interfaces. /// </summary> /// <param name="config">The network config to be filtered by.</param> /// <param name="interfaces">A list of possible interfaces to be filtered.</param> From 977d1c38b23347e79e09328e88c081246942f97f Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Sun, 18 Jan 2026 20:04:52 -0500 Subject: [PATCH 168/390] Update issue template version to 10.11.6 --- .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 a505d4168f..9bcff76bd8 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.6 - 10.11.5 - 10.11.4 - 10.11.3 From bab4e620e3992da91c3df7602ce64649884172f2 Mon Sep 17 00:00:00 2001 From: Aung Khant Soe <aungkhantsoe4@gmail.com> Date: Sun, 18 Jan 2026 22:16:33 -0500 Subject: [PATCH 169/390] Translated using Weblate (Burmese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/my/ --- Emby.Server.Implementations/Localization/Core/my.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json index 4cb4cdc757..097d0d2fba 100644 --- a/Emby.Server.Implementations/Localization/Core/my.json +++ b/Emby.Server.Implementations/Localization/Core/my.json @@ -126,5 +126,7 @@ "TaskRefreshTrickplayImages": "ထရစ်ခ်ပလေး ပုံများကို ထုတ်မည်", "TaskKeyframeExtractor": "ကီးဖရိန်များကို ထုတ်နုတ်ခြင်း", "TaskCleanCollectionsAndPlaylists": "စုစည်းမှုများနှင့် အစဉ်လိုက်ပြသမှုများကို ရှင်းလင်းမည်", - "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ" + "HearingImpaired": "အကြားအာရုံ ချို့တဲ့သူ", + "TaskDownloadMissingLyrics": "ကျန်နေသောသီချင်းစာသားများအား ဒေါင်းလုတ်ဆွဲပါ", + "TaskDownloadMissingLyricsDescription": "သီချင်းများအတွက် သီချင်းစာသား ဒေါင်းလုတ်ဆွဲပါ" } From 7151c4ca218aac3402231cfb2954d26b50dcea93 Mon Sep 17 00:00:00 2001 From: Dzmitry Zubialevich <dzmitry.zubialevich@gmail.com> Date: Mon, 19 Jan 2026 01:43:13 -0500 Subject: [PATCH 170/390] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 62ada96c00..3d598c491f 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -50,7 +50,7 @@ "User": "Карыстальнік", "UserDeletedWithName": "Карыстальнік {0} быў выдалены", "UserDownloadingItemWithValues": "{0} спампоўваецца {1}", - "TaskOptimizeDatabase": "Аптымізаваць базу дадзеных", + "TaskOptimizeDatabase": "Аптымізацыя базы даных", "Artists": "Выканаўцы", "UserOfflineFromDevice": "{0} адлучыўся ад {1}", "UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}", @@ -59,8 +59,8 @@ "TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.", "TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія сканфігураваныя на аўтаматычнае абнаўленне.", "TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.", - "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метададзеных.", - "TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых зменаў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць выдайнасць.", + "TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субцітры на аснове канфігурацыі метаданых.", + "TaskOptimizeDatabaseDescription": "Сціскае базу даных і вызваляе вольную прастору. Выкананне гэтай задачы пасля сканіравання бібліятэкі або іншых змяненняў, якія мадыфікуюць базу даных, можа палепшыць прадукцыйнасць.", "TaskKeyframeExtractor": "Экстрактар ключавых кадраў", "TasksApplicationCategory": "Праграма", "AppDeviceValues": "Праграма: {0}, Прылада: {1}", @@ -136,6 +136,6 @@ "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песняў", "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента", "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay", - "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка", + "CleanupUserDataTask": "Задача па ачыстцы даных карыстальніка", "CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён." } From 152d4451ec2fbddb867358027bc39a6ee1389bd1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:57:28 +0000 Subject: [PATCH 171/390] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 8 ++++---- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 4 ++-- .github/workflows/ci-tests.yml | 2 +- .github/workflows/commands.yml | 6 +++--- .github/workflows/issue-template-check.yml | 4 ++-- .github/workflows/release-bump-version.yaml | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 055ef7ea7c..c7a7785e97 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup .NET uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 @@ -28,13 +28,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index bb3cb50695..23a82a1b2b 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -11,7 +11,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -40,7 +40,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index d816ac054b..1bf01bf758 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -16,7 +16,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -43,7 +43,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 496c2024ae..5cb13d6947 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ matrix.os }}" steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index a70ec00eef..2c4efcc8ca 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -40,12 +40,12 @@ jobs: runs-on: ubuntu-latest steps: - name: pull in script - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14' cache: 'pip' diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 53a66e013e..dcd1fb7cfe 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -10,12 +10,12 @@ jobs: issues: write steps: - name: pull in script - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.14' cache: 'pip' diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml index d39d2cb9c3..4c6b6b8e75 100644 --- a/.github/workflows/release-bump-version.yaml +++ b/.github/workflows/release-bump-version.yaml @@ -33,7 +33,7 @@ jobs: yq-version: v4.9.8 - name: Checkout Repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ env.TAG_BRANCH }} From 62474af0c0aece2bd33d7e43b010680d57c90c40 Mon Sep 17 00:00:00 2001 From: queeup <queeup@zoho.com> Date: Sat, 24 Jan 2026 18:09:20 -0500 Subject: [PATCH 172/390] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- .../Localization/Core/tr.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 478111049f..33f129505c 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} kütüphaneye eklendi", "ItemRemovedWithName": "{0} kütüphaneden silindi", "LabelIpAddressValue": "IP adresi: {0}", - "LabelRunningTimeValue": "Çalışma süresi: {0}", + "LabelRunningTimeValue": "Oynatma süresi: {0}", "Latest": "En son", "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi", @@ -42,7 +42,7 @@ "MusicVideos": "Müzik Videoları", "NameInstallFailed": "{0} kurulumu başarısız", "NameSeasonNumber": "{0}. Sezon", - "NameSeasonUnknown": "Bilinmeyen Sezon", + "NameSeasonUnknown": "Sezon Bilinmiyor", "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.", "NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut", "NotificationOptionApplicationUpdateInstalled": "Uygulama güncellemesi yüklendi", @@ -57,7 +57,7 @@ "NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi", "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor", "NotificationOptionTaskFailed": "Zamanlanmış görev hatası", - "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi", + "NotificationOptionUserLockedOut": "Kullanıcı hesabı kilitlendi", "NotificationOptionVideoPlayback": "Video oynatma başladı", "NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu", "Photos": "Fotoğraflar", @@ -82,7 +82,7 @@ "UserCreatedWithName": "{0} kullanıcısı oluşturuldu", "UserDeletedWithName": "{0} kullanıcısı silindi", "UserDownloadingItemWithValues": "{0} kullanıcısı {1} medyasını indiriyor", - "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi", + "UserLockedOutWithName": "{0} adlı kullanıcı hesabı kilitlendi", "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi", "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi", "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi", @@ -125,15 +125,15 @@ "TaskKeyframeExtractor": "Ana Kare Çıkarıcı", "External": "Harici", "HearingImpaired": "Duyma Engelli", - "TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur", - "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.", + "TaskRefreshTrickplayImages": "Hızlı Önizleme Görsellerini Oluştur", + "TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için hızlı önizleme görselleri oluşturur.", "TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.", "TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin", "TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.", "TaskAudioNormalization": "Ses Normalleştirme", "TaskExtractMediaSegments": "Medya Segmenti Tarama", - "TaskMoveTrickplayImages": "Trickplay Görsel Konumunu Taşıma", - "TaskMoveTrickplayImagesDescription": "Mevcut trickplay dosyalarını kütüphane ayarlarına göre taşır.", + "TaskMoveTrickplayImages": "Hızlı Önizleme Görsel Konumunu Taşıma", + "TaskMoveTrickplayImagesDescription": "Mevcut hızlı önizleme dosyalarını kütüphane ayarlarına göre taşır.", "TaskDownloadMissingLyrics": "Eksik şarkı sözlerini indir", "TaskDownloadMissingLyricsDescription": "Şarkı sözlerini indirir", "TaskExtractMediaSegmentsDescription": "MediaSegment özelliği etkin olan eklentilerden medya segmentlerini çıkarır veya alır.", From 411a6cced1f27c846009d5c00e313ddd12daf267 Mon Sep 17 00:00:00 2001 From: ldmheaye <lhydeema@outlook.com> Date: Sat, 24 Jan 2026 20:27:03 -0500 Subject: [PATCH 173/390] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- .../Localization/Core/zh-CN.json | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 1bfa4e3c30..e45a51946c 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -9,56 +9,56 @@ "Channels": "频道", "ChapterNameValue": "章节 {0}", "Collections": "合集", - "DeviceOfflineWithName": "{0} 已断开", + "DeviceOfflineWithName": "{0} 已断开连接", "DeviceOnlineWithName": "{0} 已连接", "FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败", - "Favorites": "我的最爱", + "Favorites": "收藏夹", "Folders": "文件夹", "Genres": "类型", "HeaderAlbumArtists": "专辑艺术家", "HeaderContinueWatching": "继续观看", "HeaderFavoriteAlbums": "收藏的专辑", - "HeaderFavoriteArtists": "最爱的艺术家", - "HeaderFavoriteEpisodes": "最爱的剧集", - "HeaderFavoriteShows": "最爱的节目", - "HeaderFavoriteSongs": "最爱的歌曲", + "HeaderFavoriteArtists": "收藏的艺术家", + "HeaderFavoriteEpisodes": "收藏的剧集", + "HeaderFavoriteShows": "收藏的节目", + "HeaderFavoriteSongs": "收藏的歌曲", "HeaderLiveTV": "电视直播", - "HeaderNextUp": "接下来", + "HeaderNextUp": "接下来播放", "HeaderRecordingGroups": "录制组", "HomeVideos": "家庭视频", "Inherit": "继承", "ItemAddedWithName": "{0} 已添加到媒体库", - "ItemRemovedWithName": "{0} 已从媒体库中移除", + "ItemRemovedWithName": "{0} 已从媒体库移除", "LabelIpAddressValue": "IP 地址:{0}", "LabelRunningTimeValue": "运行时间:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 服务器已更新", - "MessageApplicationUpdatedTo": "Jellyfin Server 版本已更新为 {0}", + "MessageApplicationUpdatedTo": "Jellyfin 服务器版本已更新到 {0}", "MessageNamedServerConfigurationUpdatedWithValue": "服务器配置 {0} 部分已更新", "MessageServerConfigurationUpdated": "服务器配置已更新", "MixedContent": "混合内容", "Movies": "电影", "Music": "音乐", - "MusicVideos": "音乐视频", + "MusicVideos": "MV", "NameInstallFailed": "{0} 安装失败", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季", - "NewVersionIsAvailable": "Jellyfin Server 有新版本可以下载。", + "NewVersionIsAvailable": "Jellyfin 服务器有新版本可供下载。", "NotificationOptionApplicationUpdateAvailable": "有可用的应用程序更新", "NotificationOptionApplicationUpdateInstalled": "应用程序更新已安装", - "NotificationOptionAudioPlayback": "音频开始播放", + "NotificationOptionAudioPlayback": "音频已开始播放", "NotificationOptionAudioPlaybackStopped": "音频播放已停止", "NotificationOptionCameraImageUploaded": "相机图片已上传", "NotificationOptionInstallationFailed": "安装失败", "NotificationOptionNewLibraryContent": "已添加新内容", - "NotificationOptionPluginError": "插件失败", + "NotificationOptionPluginError": "插件出错", "NotificationOptionPluginInstalled": "插件已安装", "NotificationOptionPluginUninstalled": "插件已卸载", - "NotificationOptionPluginUpdateInstalled": "插件更新已安装", + "NotificationOptionPluginUpdateInstalled": "插件已更新", "NotificationOptionServerRestartRequired": "服务器需要重启", "NotificationOptionTaskFailed": "计划任务失败", "NotificationOptionUserLockedOut": "用户已锁定", - "NotificationOptionVideoPlayback": "视频开始播放", + "NotificationOptionVideoPlayback": "视频已开始播放", "NotificationOptionVideoPlaybackStopped": "视频播放已停止", "Photos": "照片", "Playlists": "播放列表", @@ -72,7 +72,7 @@ "ServerNameNeedsToBeRestarted": "{0} 需要重新启动", "Shows": "节目", "Songs": "歌曲", - "StartupEmbyServerIsLoading": "Jellyfin 服务器加载中。请稍后再试。", + "StartupEmbyServerIsLoading": "Jellyfin 服务器正在启动,请稍后再试。", "SubtitleDownloadFailureForItem": "为 {0} 下载字幕失败", "SubtitleDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的字幕", "Sync": "同步", @@ -84,11 +84,11 @@ "UserDownloadingItemWithValues": "{0} 正在下载 {1}", "UserLockedOutWithName": "用户 {0} 已被锁定", "UserOfflineFromDevice": "{0} 已从 {1} 断开", - "UserOnlineFromDevice": "{0} 在线,来自 {1}", - "UserPasswordChangedWithName": "已为用户 {0} 更改密码", - "UserPolicyUpdatedWithName": "用户协议已经被更新为 {0}", - "UserStartedPlayingItemWithValues": "{0} 已在 {2} 上开始播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", + "UserOnlineFromDevice": "{0} 已在 {1} 上线", + "UserPasswordChangedWithName": "用户 {0} 的密码已更改", + "UserPolicyUpdatedWithName": "用户协议已更新为 {0}", + "UserStartedPlayingItemWithValues": "{0} 在 {2} 上开始播放 {1}", + "UserStoppedPlayingItemWithValues": "{0} 在 {2} 上停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已添加至您的媒体库中", "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本 {0}", From a676391af2bf9f8de4c14d38f49fae3084534761 Mon Sep 17 00:00:00 2001 From: ldmheaye <lhydeema@outlook.com> Date: Sat, 24 Jan 2026 20:35:10 -0500 Subject: [PATCH 174/390] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index e45a51946c..317bbb6266 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -5,7 +5,7 @@ "Artists": "艺术家", "AuthenticationSucceededWithUserName": "{0} 认证成功", "Books": "书籍", - "CameraImageUploadedFrom": "新的相机图像已从 {0} 上传", + "CameraImageUploadedFrom": "新的相机照片已从 {0} 上传", "Channels": "频道", "ChapterNameValue": "章节 {0}", "Collections": "合集", From bc6c3b1013a8556d68bcb64969a23cfb5ce80722 Mon Sep 17 00:00:00 2001 From: ldmheaye <lhydeema@outlook.com> Date: Sat, 24 Jan 2026 20:54:13 -0500 Subject: [PATCH 175/390] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- .../Localization/Core/zh-CN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 317bbb6266..62e39b60c1 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -5,13 +5,13 @@ "Artists": "艺术家", "AuthenticationSucceededWithUserName": "{0} 认证成功", "Books": "书籍", - "CameraImageUploadedFrom": "新的相机照片已从 {0} 上传", + "CameraImageUploadedFrom": "已从 {0} 上传新的相机照片", "Channels": "频道", "ChapterNameValue": "章节 {0}", "Collections": "合集", "DeviceOfflineWithName": "{0} 已断开连接", "DeviceOnlineWithName": "{0} 已连接", - "FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败", + "FailedLoginAttemptWithUserName": "来自 {0} 的登录失败", "Favorites": "收藏夹", "Folders": "文件夹", "Genres": "类型", @@ -48,7 +48,7 @@ "NotificationOptionApplicationUpdateInstalled": "应用程序更新已安装", "NotificationOptionAudioPlayback": "音频已开始播放", "NotificationOptionAudioPlaybackStopped": "音频播放已停止", - "NotificationOptionCameraImageUploaded": "相机图片已上传", + "NotificationOptionCameraImageUploaded": "相机照片已上传", "NotificationOptionInstallationFailed": "安装失败", "NotificationOptionNewLibraryContent": "已添加新内容", "NotificationOptionPluginError": "插件出错", @@ -79,8 +79,8 @@ "System": "系统", "TvShows": "电视剧", "User": "用户", - "UserCreatedWithName": "用户 {0} 已创建", - "UserDeletedWithName": "用户 {0} 已删除", + "UserCreatedWithName": "已创建用户 {0}", + "UserDeletedWithName": "已删除用户 {0}", "UserDownloadingItemWithValues": "{0} 正在下载 {1}", "UserLockedOutWithName": "用户 {0} 已被锁定", "UserOfflineFromDevice": "{0} 已从 {1} 断开", From 441a41b2ebe9d65f7ef9fcf4546d33e62c9cdb63 Mon Sep 17 00:00:00 2001 From: ldmheaye <lhydeema@outlook.com> Date: Sat, 24 Jan 2026 23:20:11 -0500 Subject: [PATCH 176/390] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 62e39b60c1..b9635105a2 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -57,7 +57,7 @@ "NotificationOptionPluginUpdateInstalled": "插件已更新", "NotificationOptionServerRestartRequired": "服务器需要重启", "NotificationOptionTaskFailed": "计划任务失败", - "NotificationOptionUserLockedOut": "用户已锁定", + "NotificationOptionUserLockedOut": "用户已被锁定", "NotificationOptionVideoPlayback": "视频已开始播放", "NotificationOptionVideoPlaybackStopped": "视频播放已停止", "Photos": "照片", From 92f3c8cf15b265020bca9386f434a8b968a338af Mon Sep 17 00:00:00 2001 From: queeup <queeup@zoho.com> Date: Sun, 25 Jan 2026 04:50:00 -0500 Subject: [PATCH 177/390] Translated using Weblate (Turkish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/ --- Emby.Server.Implementations/Localization/Core/tr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 33f129505c..a07e6864ed 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -74,7 +74,7 @@ "Songs": "Şarkılar", "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi", + "SubtitleDownloadFailureFromForItem": "{1} için altyazılar {0} sağlayıcısından indirilemedi", "Sync": "Eşzamanlama", "System": "Sistem", "TvShows": "Diziler", @@ -98,8 +98,8 @@ "TasksLibraryCategory": "Kütüphane", "TasksMaintenanceCategory": "Bakım", "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", - "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik alt yazılar için internette arama yapar.", - "TaskDownloadMissingSubtitles": "Eksik alt yazıları indir", + "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.", + "TaskDownloadMissingSubtitles": "Eksik altyazıları indir", "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", "TaskRefreshChannels": "Kanalları Yenile", "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.", From 874fd9ac0a2410dd49b01087610b52942d8614a7 Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Sun, 25 Jan 2026 21:39:57 +0100 Subject: [PATCH 178/390] Remove some deprecated API members --- .../Controllers/ConfigurationController.cs | 20 ---------------- .../Controllers/EnvironmentController.cs | 14 ----------- .../Controllers/QuickConnectController.cs | 10 -------- Jellyfin.Api/Controllers/TvShowsController.cs | 2 -- Jellyfin.Api/Controllers/UserController.cs | 23 ------------------- .../ConfigurationDtos/MediaEncoderPathDto.cs | 17 -------------- .../StartupDtos/StartupRemoteAccessDto.cs | 8 ------- .../Models/UserDtos/UpdateUserEasyPassword.cs | 22 ------------------ .../Session/ClientCapabilities.cs | 10 -------- 9 files changed, 126 deletions(-) delete mode 100644 Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs delete mode 100644 Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 8dcaebf6db..9e03fbeb06 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -3,8 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text.Json; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; -using Jellyfin.Api.Models.ConfigurationDtos; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; @@ -143,22 +141,4 @@ public class ConfigurationController : BaseJellyfinApiController return NoContent(); } - - /// <summary> - /// Updates the path to the media encoder. - /// </summary> - /// <param name="mediaEncoderPath">Media encoder path form body.</param> - /// <response code="204">Media encoder path updated.</response> - /// <returns>Status.</returns> - [Obsolete("This endpoint is obsolete.")] - [ApiExplorerSettings(IgnoreApi = true)] - [HttpPost("MediaEncoder/Path")] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath) - { - // API ENDPOINT DISABLED (NOOP) FOR SECURITY PURPOSES - // _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); - return NoContent(); - } } diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 284a97621d..70a8c72c0f 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -128,20 +128,6 @@ public class EnvironmentController : BaseJellyfinApiController return NoContent(); } - /// <summary> - /// Gets network paths. - /// </summary> - /// <response code="200">Empty array returned.</response> - /// <returns>List of entries.</returns> - [Obsolete("This endpoint is obsolete.")] - [HttpGet("NetworkShares")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares() - { - _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); - return Array.Empty<FileSystemEntryInfo>(); - } - /// <summary> /// Gets available drives from the server's file system. /// </summary> diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 14f5265aa7..2a15ff767c 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -65,16 +65,6 @@ public class QuickConnectController : BaseJellyfinApiController } } - /// <summary> - /// Old version of <see cref="InitiateQuickConnect" /> using a GET method. - /// Still available to avoid breaking compatibility. - /// </summary> - /// <returns>The result of <see cref="InitiateQuickConnect" />.</returns> - [Obsolete("Use POST request instead")] - [HttpGet("Initiate")] - [ApiExplorerSettings(IgnoreApi = true)] - public Task<ActionResult<QuickConnectResult>> InitiateQuickConnectLegacy() => InitiateQuickConnect(); - /// <summary> /// Attempts to retrieve authentication information. /// </summary> diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 2817e3cbc7..bd6d931b21 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -69,7 +69,6 @@ public class TvShowsController : BaseJellyfinApiController /// <param name="enableUserData">Optional. Include user data.</param> /// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param> /// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param> - /// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param> /// <param name="enableResumable">Whether to include resumable episodes in next up results.</param> /// <param name="enableRewatching">Whether to include watched episodes in next up results.</param> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns> @@ -88,7 +87,6 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery][ParameterObsolete] bool disableFirstEpisode = false, [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index d0ced277a0..536b95dbb5 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -337,29 +337,6 @@ public class UserController : BaseJellyfinApiController [FromBody, Required] UpdateUserPassword request) => UpdateUserPassword(userId, request); - /// <summary> - /// Updates a user's easy password. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="request">The <see cref="UpdateUserEasyPassword"/> request.</param> - /// <response code="204">Password successfully reset.</response> - /// <response code="403">User is not allowed to update the password.</response> - /// <response code="404">User not found.</response> - /// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns> - [HttpPost("{userId}/EasyPassword")] - [Obsolete("Use Quick Connect instead")] - [ApiExplorerSettings(IgnoreApi = true)] - [Authorize] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateUserEasyPassword( - [FromRoute, Required] Guid userId, - [FromBody, Required] UpdateUserEasyPassword request) - { - return Forbid(); - } - /// <summary> /// Updates a user. /// </summary> diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs deleted file mode 100644 index 5a48345eb6..0000000000 --- a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Jellyfin.Api.Models.ConfigurationDtos; - -/// <summary> -/// Media Encoder Path Dto. -/// </summary> -public class MediaEncoderPathDto -{ - /// <summary> - /// Gets or sets media encoder path. - /// </summary> - public string Path { get; set; } = null!; - - /// <summary> - /// Gets or sets media encoder path type. - /// </summary> - public string PathType { get; set; } = null!; -} diff --git a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs index 9c29e372cf..2a1a312d54 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupRemoteAccessDto.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations; namespace Jellyfin.Api.Models.StartupDtos; @@ -13,11 +12,4 @@ public class StartupRemoteAccessDto /// </summary> [Required] public bool EnableRemoteAccess { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether enable automatic port mapping. - /// </summary> - [Required] - [Obsolete("No longer supported")] - public bool EnableAutomaticPortMapping { get; set; } } diff --git a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs b/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs deleted file mode 100644 index f19d0b57a1..0000000000 --- a/Jellyfin.Api/Models/UserDtos/UpdateUserEasyPassword.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Jellyfin.Api.Models.UserDtos; - -/// <summary> -/// The update user easy password request body. -/// </summary> -public class UpdateUserEasyPassword -{ - /// <summary> - /// Gets or sets the new sha1-hashed password. - /// </summary> - public string? NewPassword { get; set; } - - /// <summary> - /// Gets or sets the new password. - /// </summary> - public string? NewPw { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether to reset the password. - /// </summary> - public bool ResetPassword { get; set; } -} diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index fc1f24ae16..9b243c1173 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -31,15 +31,5 @@ namespace MediaBrowser.Model.Session public string AppStoreUrl { get; set; } public string IconUrl { get; set; } - - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsContentUploading { get; set; } = false; - - // TODO: Remove after 10.9 - [Obsolete("Unused")] - [DefaultValue(false)] - public bool? SupportsSync { get; set; } = false; } } From b8d2f1f9115ed3ed18960fcebd1188a3fb7d2f5e Mon Sep 17 00:00:00 2001 From: Niels van Velzen <git@ndat.nl> Date: Tue, 27 Jan 2026 11:13:32 +0100 Subject: [PATCH 179/390] Remove unused usings --- Jellyfin.Api/Controllers/EnvironmentController.cs | 1 - Jellyfin.Api/Controllers/TvShowsController.cs | 1 - MediaBrowser.Model/Session/ClientCapabilities.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 70a8c72c0f..794ca96932 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; -using Jellyfin.Api.Constants; using Jellyfin.Api.Models.EnvironmentDtos; using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index bd6d931b21..c86c9b8f61 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index 9b243c1173..597845fc17 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Jellyfin.Data.Enums; using MediaBrowser.Model.Dlna; From b9e5cce3838ec0cc89c67c48d8c2c40348bebc6f Mon Sep 17 00:00:00 2001 From: Weblate <noreply@weblate.org> Date: Tue, 27 Jan 2026 14:12:25 -0500 Subject: [PATCH 180/390] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ --- Emby.Server.Implementations/Localization/Core/ar.json | 1 - Emby.Server.Implementations/Localization/Core/bg-BG.json | 1 - Emby.Server.Implementations/Localization/Core/ca.json | 1 - Emby.Server.Implementations/Localization/Core/cs.json | 1 - Emby.Server.Implementations/Localization/Core/da.json | 1 - Emby.Server.Implementations/Localization/Core/de.json | 1 - Emby.Server.Implementations/Localization/Core/el.json | 1 - Emby.Server.Implementations/Localization/Core/en-GB.json | 1 - Emby.Server.Implementations/Localization/Core/es-AR.json | 1 - Emby.Server.Implementations/Localization/Core/es-MX.json | 1 - Emby.Server.Implementations/Localization/Core/es.json | 1 - Emby.Server.Implementations/Localization/Core/fa.json | 1 - Emby.Server.Implementations/Localization/Core/fr-CA.json | 1 - Emby.Server.Implementations/Localization/Core/fr.json | 1 - Emby.Server.Implementations/Localization/Core/gsw.json | 1 - Emby.Server.Implementations/Localization/Core/he.json | 1 - Emby.Server.Implementations/Localization/Core/hr.json | 1 - Emby.Server.Implementations/Localization/Core/hu.json | 1 - Emby.Server.Implementations/Localization/Core/it.json | 1 - Emby.Server.Implementations/Localization/Core/kk.json | 1 - Emby.Server.Implementations/Localization/Core/ko.json | 1 - Emby.Server.Implementations/Localization/Core/lt-LT.json | 1 - Emby.Server.Implementations/Localization/Core/ms.json | 1 - Emby.Server.Implementations/Localization/Core/nb.json | 1 - Emby.Server.Implementations/Localization/Core/nl.json | 1 - Emby.Server.Implementations/Localization/Core/pl.json | 1 - Emby.Server.Implementations/Localization/Core/pt-BR.json | 1 - Emby.Server.Implementations/Localization/Core/pt-PT.json | 1 - Emby.Server.Implementations/Localization/Core/ru.json | 1 - Emby.Server.Implementations/Localization/Core/sk.json | 1 - Emby.Server.Implementations/Localization/Core/sl-SI.json | 1 - Emby.Server.Implementations/Localization/Core/sv.json | 1 - Emby.Server.Implementations/Localization/Core/tr.json | 1 - Emby.Server.Implementations/Localization/Core/zh-CN.json | 1 - Emby.Server.Implementations/Localization/Core/zh-HK.json | 1 - 35 files changed, 35 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index d09a7884e7..7ce8baef59 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -73,7 +73,6 @@ "Shows": "العروض", "Songs": "الأغاني", "StartupEmbyServerIsLoading": "يتم تحميل خادم Jellyfin . الرجاء المحاولة بعد قليل.", - "SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}", "SubtitleDownloadFailureFromForItem": "فشل تحميل الترجمات من {0} ل {1}", "Sync": "مزامنة", "System": "النظام", diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index fd3666ef1c..92b8e5d565 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -73,7 +73,6 @@ "Shows": "Сериали", "Songs": "Песни", "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.", - "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}", "SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени", "Sync": "Синхронизиране", "System": "Система", diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 596df63482..82cc1857b7 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -73,7 +73,6 @@ "Shows": "Sèries", "Songs": "Cançons", "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}", "Sync": "Sincronitza", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index e14edcffa2..4d2477044f 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -73,7 +73,6 @@ "Shows": "Seriály", "Songs": "Skladby", "StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.", - "SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}", "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo", "Sync": "Synchronizace", "System": "Systém", diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index bbee38ba5f..8b0d8745dc 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -73,7 +73,6 @@ "Shows": "Serier", "Songs": "Sange", "StartupEmbyServerIsLoading": "Jellyfin er i gang med at starte. Prøv igen om et øjeblik.", - "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}", "Sync": "Synkroniser", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 0b042c8fed..e9a1630d9d 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -73,7 +73,6 @@ "Shows": "Serien", "Songs": "Lieder", "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.", - "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}", "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden", "Sync": "Synchronisation", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 2ba2085da2..87362ff8e0 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -73,7 +73,6 @@ "Shows": "Σειρές", "Songs": "Τραγούδια", "StartupEmbyServerIsLoading": "Ο διακομιστής Jellyfin φορτώνει. Περιμένετε λίγο και δοκιμάστε ξανά.", - "SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}", "SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}", "Sync": "Συγχρονισμός", "System": "Σύστημα", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 720f550b33..bd5be0b1fc 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -73,7 +73,6 @@ "Shows": "Shows", "Songs": "Songs", "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "Sync": "Sync", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index 1f8af4c8a5..ce0044f643 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -73,7 +73,6 @@ "Shows": "Series", "Songs": "Canciones", "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 2830c657b6..6748fff4cc 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -73,7 +73,6 @@ "Shows": "Programas", "Songs": "Canciones", "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.", - "SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}", "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 1ec5eaa2a8..b9c57afe6d 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -73,7 +73,6 @@ "Shows": "Series", "Songs": "Canciones", "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", - "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}", "SubtitleDownloadFailureFromForItem": "Fallo en la descarga de subtítulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index ff14c13678..90cd3a58e9 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -73,7 +73,6 @@ "Shows": "سریال‌ها", "Songs": "موسیقی‌ها", "StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.", - "SubtitleDownloadFailureForItem": "دانلود زیرنویس برای {0} ناموفق بود", "SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد", "Sync": "همگام‌سازی", "System": "سیستم", diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 6d079d2f57..a8964e8b62 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -73,7 +73,6 @@ "Shows": "Séries", "Songs": "Chansons", "StartupEmbyServerIsLoading": "Serveur Jellyfin en cours de chargement. Réessayez dans quelques instants.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", "Sync": "Synchroniser", "System": "Système", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 8bf41c02a7..b2a2e502ab 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -73,7 +73,6 @@ "Shows": "Séries", "Songs": "Chansons", "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", - "SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", "Sync": "Synchroniser", "System": "Système", diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index e1ee8cf7c4..9be6f05ee1 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -73,7 +73,6 @@ "Shows": "Serie", "Songs": "Lieder", "StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde", "Sync": "Synchronisation", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 90c921898f..ef95a639f6 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -73,7 +73,6 @@ "Shows": "סדרות", "Songs": "שירים", "StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה", "Sync": "סנכרון", "System": "מערכת", diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 67263d3b22..eb75cfd491 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -73,7 +73,6 @@ "Shows": "Serije", "Songs": "Pjesme", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", - "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}", "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", "Sync": "Sinkronizacija", "System": "Sustav", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 81a996330b..813d799237 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -73,7 +73,6 @@ "Shows": "Sorozatok", "Songs": "Számok", "StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}", "Sync": "Szinkronizálás", "System": "Rendszer", diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 421c4ee306..c2974704be 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -73,7 +73,6 @@ "Shows": "Serie TV", "Songs": "Brani", "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", - "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}", "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", "Sync": "Sincronizza", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index e050196bc9..fc5fcf3c4d 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -73,7 +73,6 @@ "Shows": "Körsetımder", "Songs": "Äuender", "StartupEmbyServerIsLoading": "Jellyfin Server jüktelude. Ärekettı köp ūzamai qaitalañyz.", - "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз", "SubtitleDownloadFailureFromForItem": "{1} üşın subtitrlerdı {0} közınen jüktep alu sätsız", "Sync": "Ündestıru", "System": "Jüie", diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 3d1b1ed271..2b24ea2c8b 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -73,7 +73,6 @@ "Shows": "시리즈", "Songs": "노래", "StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다", "Sync": "동기화", "System": "시스템", diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 3918ab81c6..bdf63b4ca0 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -73,7 +73,6 @@ "Shows": "Laidos", "Songs": "Kūriniai", "StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}", "Sync": "Sinchronizuoti", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 971f79c2c8..2be04be80a 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -73,7 +73,6 @@ "Shows": "Tayangan", "Songs": "Lagu-lagu", "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}", "Sync": "Segerak", "System": "Sistem", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index e73c56cb90..cd03157200 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -73,7 +73,6 @@ "Shows": "Serier", "Songs": "Sanger", "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", - "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}", "Sync": "Synkroniser", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 09246bd110..534c64e93c 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -73,7 +73,6 @@ "Shows": "Series", "Songs": "Nummers", "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden. Probeer het later opnieuw.", - "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt", "SubtitleDownloadFailureFromForItem": "Ondertiteling kon niet gedownload worden van {0} voor {1}", "Sync": "Synchronisatie", "System": "Systeem", diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index 8ca22ac046..f1c19ac1d9 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -73,7 +73,6 @@ "Shows": "Seriale", "Songs": "Utwory", "StartupEmbyServerIsLoading": "Trwa wczytywanie serwera Jellyfin. Spróbuj ponownie za chwilę.", - "SubtitleDownloadFailureForItem": "Pobieranie napisów dla {0} zakończone niepowodzeniem", "SubtitleDownloadFailureFromForItem": "Nieudane pobieranie napisów z {0} dla {1}", "Sync": "Synchronizacja", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index dc5bff161a..8e76c6c63c 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -73,7 +73,6 @@ "Shows": "Séries", "Songs": "Músicas", "StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor, tente novamente mais tarde.", - "SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}", "SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 17284854f6..a270364935 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -73,7 +73,6 @@ "Shows": "Séries", "Songs": "Músicas", "StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}", "Sync": "Sincronização", "System": "Sistema", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 1470a538c2..03bce0ebdf 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -73,7 +73,6 @@ "Shows": "Сериалы", "Songs": "Композиции", "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", - "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", "Sync": "Синхронизация", "System": "Система", diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 1de78eeaeb..7c8d860476 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -73,7 +73,6 @@ "Shows": "Seriály", "Songs": "Skladby", "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.", - "SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo", "SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo", "Sync": "Synchronizácia", "System": "Systém", diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index ff92db2f2d..7c7c88e28a 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -73,7 +73,6 @@ "Shows": "Serije", "Songs": "Pesmi", "StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}", "Sync": "Sinhroniziraj", "System": "Sistem", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 1ee1a53662..23acd3c532 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -73,7 +73,6 @@ "Shows": "Serier", "Songs": "Låtar", "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.", - "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades", "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}", "Sync": "Synk", "System": "System", diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index a07e6864ed..d13f662e4c 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -73,7 +73,6 @@ "Shows": "Diziler", "Songs": "Şarkılar", "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{1} için altyazılar {0} sağlayıcısından indirilemedi", "Sync": "Eşzamanlama", "System": "Sistem", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index b9635105a2..0a0795d41b 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -73,7 +73,6 @@ "Shows": "节目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "Jellyfin 服务器正在启动,请稍后再试。", - "SubtitleDownloadFailureForItem": "为 {0} 下载字幕失败", "SubtitleDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的字幕", "Sync": "同步", "System": "系统", diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index c8800e256e..e57a0c5b09 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -73,7 +73,6 @@ "Shows": "節目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "正在載入 Jellyfin,請稍後再試。", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", "Sync": "同步", "System": "系統", From b97f5b809daa5e64bc54eb6534ed4c4f69ff6cd9 Mon Sep 17 00:00:00 2001 From: Gladtbam <49943258+Gladtbam@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:40:12 +0800 Subject: [PATCH 181/390] fix: Resolve CA1849/CA2007 synchronous IO in EpubImageProvider (#16124) --- .../OpenPackagingFormat/EpubImageProvider.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs index 33d2823de6..69cae77628 100644 --- a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs @@ -48,13 +48,13 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat { if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase)) { - return GetFromZip(item); + return GetFromZip(item, cancellationToken); } return Task.FromResult(new DynamicImageResponse { HasImage = false }); } - private async Task<DynamicImageResponse> LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory) + private async Task<DynamicImageResponse> LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory, CancellationToken cancellationToken) { var utilities = new OpfReader<EpubImageProvider>(opf, _logger); var coverReference = utilities.ReadCoverPath(opfRootDirectory); @@ -72,9 +72,11 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat } var memoryStream = new MemoryStream(); - using (var coverStream = coverFile.Open()) + + var coverStream = await coverFile.OpenAsync(cancellationToken).ConfigureAwait(false); + await using (coverStream.ConfigureAwait(false)) { - await coverStream.CopyToAsync(memoryStream).ConfigureAwait(false); + await coverStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); } memoryStream.Position = 0; @@ -85,9 +87,9 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat return response; } - private async Task<DynamicImageResponse> GetFromZip(BaseItem item) + private async Task<DynamicImageResponse> GetFromZip(BaseItem item, CancellationToken cancellationToken) { - using var epub = ZipFile.OpenRead(item.Path); + using var epub = await ZipFile.OpenReadAsync(item.Path, cancellationToken).ConfigureAwait(false); var opfFilePath = EpubUtils.ReadContentFilePath(epub); if (opfFilePath == null) @@ -107,12 +109,12 @@ namespace MediaBrowser.Providers.Books.OpenPackagingFormat return new DynamicImageResponse { HasImage = false }; } - using var opfStream = opfFile.Open(); + using var opfStream = await opfFile.OpenAsync(cancellationToken).ConfigureAwait(false); var opfDocument = new XmlDocument(); opfDocument.Load(opfStream); - return await LoadCover(epub, opfDocument, opfRootDirectory).ConfigureAwait(false); + return await LoadCover(epub, opfDocument, opfRootDirectory, cancellationToken).ConfigureAwait(false); } } } From 4344d756947f67b7b28ed9d18e9651194dd893d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 02:41:06 +0000 Subject: [PATCH 182/390] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-openapi.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index c7a7785e97..36844a14c4 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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 1bf01bf758..08eedd54f7 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -133,7 +133,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (unstable) into place - uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 + uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -194,7 +194,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (stable) into place - uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 + uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" From ec4744709df1664f263937ae41baadc48d7d6c9b Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:23 -0500 Subject: [PATCH 183/390] Backport pull request #15816 from jellyfin/release-10.11.z Fix artist display order Original-merge: a2b1936e73f6638bf07d5e1afd339a1e4404027a Merged-by: joshuaboniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Dto/DtoService.cs | 53 +++++++------------ .../Item/BaseItemRepository.cs | 17 +++++- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index c5dc3b054c..b392340f71 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1051,16 +1051,16 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) - .Where(e => e.Value.Length > 0) - .Select(i => - { - return new NameGuidPair - { - Name = i.Key, - Id = i.Value.First().Id - }; - }).Where(i => i is not null).ToArray(); + var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]); + + dto.ArtistItems = hasArtist.Artists + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct() + .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0 + ? new NameGuidPair { Name = name, Id = artists[0].Id } + : null) + .Where(item => item is not null) + .ToArray(); } if (item is IHasAlbumArtist hasAlbumArtist) @@ -1085,31 +1085,16 @@ namespace Emby.Server.Implementations.Dto // }) // .ToList(); + var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]); + dto.AlbumArtists = hasAlbumArtist.AlbumArtists - // .Except(foundArtists, new DistinctNameComparer()) - .Select(i => - { - // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrEmpty(i)) - { - return null; - } - - var artist = _libraryManager.GetArtist(i, new DtoOptions(false) - { - EnableImages = false - }); - if (artist is not null) - { - return new NameGuidPair - { - Name = artist.Name, - Id = artist.Id - }; - } - - return null; - }).Where(i => i is not null).ToArray(); + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct() + .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0 + ? new NameGuidPair { Name = name, Id = albumArtists[0].Id } + : null) + .Where(item => item is not null) + .ToArray(); } // Add video info diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 90aa3a22ee..43a3cdf78f 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2682,6 +2682,21 @@ public sealed class BaseItemRepository .Where(e => artistNames.Contains(e.Name)) .ToArray(); - return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray()); + var lookup = artists + .GroupBy(e => e.Name!) + .ToDictionary( + g => g.Key, + g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray()); + + var result = new Dictionary<string, MusicArtist[]>(artistNames.Count); + foreach (var name in artistNames) + { + if (lookup.TryGetValue(name, out var artistArray)) + { + result[name] = artistArray; + } + } + + return result; } } From 5045c2e4488bf60965b1f5f4893a3539bd7c3da0 Mon Sep 17 00:00:00 2001 From: IceStormNG <IceStormNG@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:24 -0500 Subject: [PATCH 184/390] Backport pull request #16053 from jellyfin/release-10.11.z Fix HLS playlist generation for transcodes with fractional framerate Original-merge: 893a849f28b651657b3797d1711da8f696b4120c Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 15b04051f4..f80b36c390 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1400,10 +1400,20 @@ public class DynamicHlsController : BaseJellyfinApiController cancellationTokenSource.Token) .ConfigureAwait(false); var mediaSourceId = state.BaseRequest.MediaSourceId; + 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) + { + double nearestIntFramerate = Math.Ceiling(fps); + segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps)); + } + var request = new CreateMainPlaylistRequest( mediaSourceId is null ? null : Guid.Parse(mediaSourceId), state.MediaPath, - state.SegmentLength * 1000, + segmentLength, state.RunTimeTicks ?? 0, state.Request.SegmentContainer ?? string.Empty, "hls1/main/", From dad6f650bc6b2dd6cd9acc69ca009f5e624c7d81 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:25 -0500 Subject: [PATCH 185/390] Backport pull request #16066 from jellyfin/release-10.11.z Fix TMDB crew department mapping Original-merge: 673f617994da6ff6a45cf428a3ea47de59edc6c5 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index d6e66a0e61..bdac57dac8 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <returns>The Jellyfin person type.</returns> public static PersonKind MapCrewToPersonType(Crew crew) { - if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) + if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase) && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Director; @@ -82,7 +82,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase) - && crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase)) + && (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase))) { return PersonKind.Writer; } From 6d34f605a3579568b966d44c1a9379b96b76bfe4 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:26 -0500 Subject: [PATCH 186/390] Backport pull request #16071 from jellyfin/release-10.11.z Rehydrate cached UserData after reattachment Original-merge: 95d08b264f68a4348d18746543882356465be3b0 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 43a3cdf78f..338544ab96 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -762,16 +762,30 @@ public sealed class BaseItemRepository await using (dbContext.ConfigureAwait(false)) { - var userKeys = item.GetUserDataKeys().ToArray(); - var retentionDate = (DateTime?)null; - await dbContext.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdateAsync( - e => e - .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate), - cancellationToken).ConfigureAwait(false); + var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + + // Rehydrate the cached userdata + item.UserData = await dbContext.UserData + .AsNoTracking() + .Where(e => e.ItemId == item.Id) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } } } From 0c274af72c1d223c0fea7f4f4ac99e04f1e1e904 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:28 -0500 Subject: [PATCH 187/390] Backport pull request #16077 from jellyfin/release-10.11.z Revert hidden directory ignore pattern Original-merge: 644327eb762a907328c68ab9f5d61a151cd96897 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 5 ++++- .../Library/IgnorePatternsTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5fac2f6b0a..59ccb9e2c7 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library "**/lost+found", "**/subs/**", "**/subs", + "**/.snapshots/**", + "**/.snapshots", + "**/.snapshot/**", + "**/.snapshot", // Trickplay files "**/*.trickplay", @@ -83,7 +87,6 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", - "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 4cb6cb9607..07061cfc77 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/.hiddendir/file.mp4", false)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } From 815a153b940aa5507923f1b22b73cd146367e1ea Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:29 -0500 Subject: [PATCH 188/390] Backport pull request #16098 from jellyfin/release-10.11.z Fix random sort returning duplicate items Original-merge: a37ead86df161d6a50d280dcac6f11294301c7e8 Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 338544ab96..5bb4494dd2 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -295,6 +295,25 @@ public sealed class BaseItemRepository dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); + + var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random); + if (hasRandomSort) + { + var orderedIds = dbQuery.Select(e => e.Id).ToList(); + if (orderedIds.Count == 0) + { + return Array.Empty<BaseItemDto>(); + } + + var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter) + .AsEnumerable() + .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)) + .Where(dto => dto is not null) + .ToDictionary(i => i!.Id); + + return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!; + } + dbQuery = ApplyNavigations(dbQuery, filter); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!; From 841e4dabb513c9c94bcbb0005d19e2a8be6434a5 Mon Sep 17 00:00:00 2001 From: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:11:30 -0500 Subject: [PATCH 189/390] Backport pull request #16109 from jellyfin/release-10.11.z Fix SessionInfoWebSocketListener not using SessionInfoDto Original-merge: e65aff8bc67e3cc97d2ebe141de9ff6a8681d792 Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Session/SessionManager.cs | 3 ++- .../SessionInfoWebSocketListener.cs | 15 +++++++++------ .../Session/ISessionManager.cs | 7 +++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index bbe23f8df3..8e14f5bdf4 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1186,7 +1186,8 @@ namespace Emby.Server.Implementations.Session return session; } - private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) + /// <inheritdoc /> + public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) { return new SessionInfoDto { diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 143d82bac6..db24c97460 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -15,7 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners; /// <summary> /// Class SessionInfoWebSocketListener. /// </summary> -public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState> +public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfoDto>, WebSocketListenerState> { private readonly ISessionManager _sessionManager; private bool _disposed; @@ -52,24 +53,26 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume /// Gets the data to send. /// </summary> /// <returns>Task{SystemInfo}.</returns> - protected override Task<IEnumerable<SessionInfo>> GetDataToSend() + protected override Task<IEnumerable<SessionInfoDto>> GetDataToSend() { - return Task.FromResult(_sessionManager.Sessions); + return Task.FromResult(_sessionManager.Sessions.Select(_sessionManager.ToSessionInfoDto)); } /// <inheritdoc /> - protected override Task<IEnumerable<SessionInfo>> GetDataToSendForConnection(IWebSocketConnection connection) + protected override Task<IEnumerable<SessionInfoDto>> GetDataToSendForConnection(IWebSocketConnection connection) { + var sessions = _sessionManager.Sessions; + // For non-admin users, filter the sessions to only include their own sessions if (connection.AuthorizationInfo?.User is not null && !connection.AuthorizationInfo.IsApiKey && !connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { var userId = connection.AuthorizationInfo.User.Id; - return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId))); + sessions = sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId)); } - return Task.FromResult(_sessionManager.Sessions); + return Task.FromResult(sessions.Select(_sessionManager.ToSessionInfoDto)); } /// <inheritdoc /> diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 2b3afa1174..c11c65c334 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -350,5 +350,12 @@ namespace MediaBrowser.Controller.Session /// <param name="sessionIdOrPlaySessionId">The session id or playsession id.</param> /// <returns>Task.</returns> Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId); + + /// <summary> + /// Gets the dto for session info. + /// </summary> + /// <param name="sessionInfo">The session info.</param> + /// <returns><see cref="SessionInfoDto"/> of the session.</returns> + SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo); } } From 0413a8b6d29f0718e7ac9c97b8658c5aedf66e4e Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:58:34 +0900 Subject: [PATCH 190/390] 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 +{ + /// <inheritdoc /> + public class IsbnExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "ISBN"; + + /// <inheritdoc /> + public string Key => "ISBN"; + + /// <inheritdoc /> + public ExternalIdMediaType? Type => null; + + /// <inheritdoc /> + 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; + +/// <inheritdoc/> +public class IsbnExternalUrlProvider : IExternalUrlProvider +{ + /// <inheritdoc/> + public string Name => "ISBN"; + + /// <inheritdoc /> + public IEnumerable<string> 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 +{ + /// <inheritdoc /> + public class ComicVineExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "Comic Vine"; + + /// <inheritdoc /> + public string Key => "ComicVine"; + + /// <inheritdoc /> + public ExternalIdMediaType? Type => null; + + /// <inheritdoc /> + 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; + +/// <inheritdoc/> +public class ComicVineExternalUrlProvider : IExternalUrlProvider +{ + /// <inheritdoc/> + public string Name => "Comic Vine"; + + /// <inheritdoc /> + public IEnumerable<string> 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 +{ + /// <inheritdoc /> + public class ComicVinePersonExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "Comic Vine"; + + /// <inheritdoc /> + public string Key => "ComicVine"; + + /// <inheritdoc /> + public ExternalIdMediaType? Type => ExternalIdMediaType.Person; + + /// <inheritdoc /> + 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 +{ + /// <inheritdoc /> + public class GoogleBooksExternalId : IExternalId + { + /// <inheritdoc /> + public string ProviderName => "Google Books"; + + /// <inheritdoc /> + public string Key => "GoogleBooks"; + + /// <inheritdoc /> + public ExternalIdMediaType? Type => null; + + /// <inheritdoc /> + 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; + +/// <inheritdoc/> +public class GoogleBooksExternalUrlProvider : IExternalUrlProvider +{ + /// <inheritdoc /> + public string Name => "Google Books"; + + /// <inheritdoc /> + public IEnumerable<string> GetExternalUrls(BaseItem item) + { + if (item.TryGetProviderId("GoogleBooks", out var externalId)) + { + if (item is Book) + { + yield return $"https://books.google.com/books?id={externalId}"; + } + } + } +} From 0a1dd56af6b5344fedfc1c7ddd08e415577c3277 Mon Sep 17 00:00:00 2001 From: DerMaddis <eulermat16@gmail.com> Date: Fri, 30 Jan 2026 14:24:11 +0100 Subject: [PATCH 191/390] TmdbSeriesProvider: Set ProductionYear field --- MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 82d4e58384..93042c3252 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -279,6 +279,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.EndDate = seriesResult.LastAirDate; series.PremiereDate = seriesResult.FirstAirDate; + series.ProductionYear = seriesResult.FirstAirDate?.Year; var ids = seriesResult.ExternalIds; if (ids is not null) From 3920ed4b9202a969937b8b2817e6ec7db327a67d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 18:09:41 +0000 Subject: [PATCH 192/390] Update dependency MetaBrainz.MusicBrainz to v8 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 242cc77e6c..021288b4a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> - <PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" /> + <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> From 7b10888c95c814af342926269403b93677760042 Mon Sep 17 00:00:00 2001 From: Riri <Daydreamerriri@outlook.com> Date: Sun, 1 Feb 2026 15:33:07 +0800 Subject: [PATCH 193/390] Remove handling for 5-letter language codes in TMDb language normalization --- MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index bdac57dac8..0944b557e9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -117,14 +117,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode); languages.Add(preferredLanguage); - - if (preferredLanguage.Length == 5) // Like en-US - { - // Currently, TMDb supports 2-letter language codes only. - // They are planning to change this in the future, thus we're - // supplying both codes if we're having a 5-letter code. - languages.Add(preferredLanguage.Substring(0, 2)); - } } languages.Add("null"); From d1d4fe2e3375487cb87d87e44ec5d4653f3b8513 Mon Sep 17 00:00:00 2001 From: Augusto <tasken@gmail.com> Date: Sun, 1 Feb 2026 09:23:54 -0500 Subject: [PATCH 194/390] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- Emby.Server.Implementations/Localization/Core/es-AR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index ce0044f643..2bbf0d5140 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -20,7 +20,7 @@ "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Capítulos favoritos", - "HeaderFavoriteShows": "Programas favoritos", + "HeaderFavoriteShows": "Series favoritas", "HeaderFavoriteSongs": "Canciones favoritas", "HeaderLiveTV": "TV en vivo", "HeaderNextUp": "Siguiente", From 4f695bc58afaf12383dddc2ee4dc8582ab100912 Mon Sep 17 00:00:00 2001 From: KecskeTech <teonyitas@gmail.com> Date: Sun, 1 Feb 2026 06:26:49 -0500 Subject: [PATCH 195/390] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- Emby.Server.Implementations/Localization/Core/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 813d799237..7d72c1f304 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -55,7 +55,7 @@ "NotificationOptionPluginInstalled": "Bővítmény telepítve", "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve", - "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges", + "NotificationOptionServerRestartRequired": "A szerver újraindítása szükséges", "NotificationOptionTaskFailed": "Hiba az ütemezett feladatban", "NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionVideoPlayback": "Videólejátszás elkezdve", From 77ff451e60fa1fee31da1f5987e6e47a22244bb8 Mon Sep 17 00:00:00 2001 From: Shadowghost <Ghost_of_Stone@web.de> Date: Sun, 1 Feb 2026 21:19:20 +0100 Subject: [PATCH 196/390] 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 <Ghost_of_Stone@web.de> Date: Sun, 1 Feb 2026 21:51:36 +0100 Subject: [PATCH 197/390] 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 8083ab78b558e4081caf4fd321e5ad8f74844f3e Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Mon, 2 Feb 2026 20:46:28 +0100 Subject: [PATCH 198/390] Fix tests --- .../Library/PathExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index 74cd303bab..650d67b195 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -26,8 +26,8 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")] [InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")] [InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")] - [InlineData("Superman: Red Son {tmdbid=3)", "tmdbid", "3")] - [InlineData("Superman: Red Son (tvdbid-6}", "tvdbid", "6")] + [InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")] + [InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")] [InlineData("[tmdbid=618355]", "tmdbid", "618355")] [InlineData("{tmdbid=618355}", "tmdbid", "618355")] [InlineData("(tmdbid=618355)", "tmdbid", "618355")] From 613d72fa26375b29b6c5da7933feff8ace1ac54e Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Tue, 3 Feb 2026 01:16:25 -0500 Subject: [PATCH 199/390] Skip empty ViewType validation --- .../Controllers/DisplayPreferencesController.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 585318d245..ef54e9db54 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -191,9 +191,17 @@ public class DisplayPreferencesController : BaseJellyfinApiController foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase))) { - if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _)) + var viewType = displayPreferences.CustomPrefs[key]; + + if (string.IsNullOrEmpty(viewType)) { - _logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]); + displayPreferences.CustomPrefs.Remove(key); + continue; + } + + if (!Enum.TryParse<ViewType>(viewType, true, out _)) + { + _logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType); displayPreferences.CustomPrefs.Remove(key); } } From 7c200899d71cc255feb58a110f0fce244bffa0d8 Mon Sep 17 00:00:00 2001 From: Blackspirits <blackspirits@gmail.com> Date: Mon, 2 Feb 2026 05:23:44 -0500 Subject: [PATCH 200/390] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index a270364935..c2ce2ba40f 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -124,8 +124,8 @@ "TaskKeyframeExtractor": "Extrator de Quadros-chave", "External": "Externo", "HearingImpaired": "Surdo", - "TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay", - "TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.", + "TaskRefreshTrickplayImages": "Gerar imagens de trickplay", + "TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", From 32d80861211031e5ee66d5068679f43335e2bcbd Mon Sep 17 00:00:00 2001 From: Blackspirits <blackspirits@gmail.com> Date: Mon, 2 Feb 2026 05:23:53 -0500 Subject: [PATCH 201/390] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 74bb1c63a6..9ae346e253 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -124,8 +124,8 @@ "HearingImpaired": "Problemas auditivos", "TaskKeyframeExtractor": "Extrator de quadro-chave", "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.", - "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo", - "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.", + "TaskRefreshTrickplayImages": "Gerar imagens de trickplay", + "TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.", "TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.", "TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução", "TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.", From 4c751e0a86d805314023b8a750e675f5131ee613 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Tue, 3 Feb 2026 17:41:01 -0500 Subject: [PATCH 202/390] Normalize names and roles --- Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 355ed64797..e2569241d2 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -74,9 +74,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I /// <inheritdoc /> public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people) { - foreach (var item in people.Where(e => e.Role is null)) + foreach (var person in people) { - item.Role = string.Empty; + person.Name = person.Name.Trim(); + person.Role = person.Role?.Trim() ?? string.Empty; } // multiple metadata providers can provide the _same_ person From ccd042750df8389c1622aaa049997c771f32f181 Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Thu, 5 Feb 2026 16:39:52 -0500 Subject: [PATCH 203/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index eb75cfd491..ebe292c22e 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}", "Channels": "Kanali", "ChapterNameValue": "Poglavlje {0}", - "Collections": "Kolekcije", + "Collections": "Zbirke", "DeviceOfflineWithName": "{0} je prekinuo vezu", "DeviceOnlineWithName": "{0} je povezan", "FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}", From fead4acae121c1a2edacebbba860bdf4f6b56d25 Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Fri, 6 Feb 2026 17:04:38 -0500 Subject: [PATCH 204/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index ebe292c22e..94db435715 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -70,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskStartedWithName": "{0} pokrenuto", "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti", - "Shows": "Serije", + "Shows": "Emisije", "Songs": "Pjesme", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", From 1f6768178a935061354c5f36c56ddd546d97cb2c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:27:55 -0700 Subject: [PATCH 205/390] Update dependency Svg.Skia to 3.4.1 (#15941) 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 021288b4a3..e1bc84dacc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> - <PackageVersion Include="Svg.Skia" Version="3.2.1" /> + <PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> <PackageVersion Include="System.Text.Json" Version="10.0.2" /> From 4a494271dd93aa41838aff0c4c9551544cbdb07f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:29:54 -0700 Subject: [PATCH 206/390] Update dependency AsyncKeyedLock to 8.0.1 (#16096) 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 e1bc84dacc..ea4436489a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ </PropertyGroup> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <ItemGroup Label="Package Dependencies"> - <PackageVersion Include="AsyncKeyedLock" Version="8.0.0" /> + <PackageVersion Include="AsyncKeyedLock" Version="8.0.1" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" /> From 21bb702fd3b03422ff0b057c3bdaf3d5639321b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:30:03 -0700 Subject: [PATCH 207/390] Update github/codeql-action action to v4.32.2 (#16171) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .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 36844a14c4..9072fa9f9e 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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 From fa4d51c5e67550a99bb67e915f676c2c2b8ab993 Mon Sep 17 00:00:00 2001 From: JPVenson <github@jpb.email> Date: Sat, 7 Feb 2026 18:50:08 +0000 Subject: [PATCH 208/390] Fix container and updated --- .devcontainer/devcontainer.json | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8b6b12c31e..c67c292372 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,31 @@ { "name": "Development Jellyfin Server", - "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", + "image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble", "service": "app", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", - // reads the extensions list and installs them - "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", + // The previous way of installing extensions via the vs command dont work on selfhosted devcontainers + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "editorconfig.editorconfig", + "github.vscode-github-actions", + "ms-dotnettools.vscode-dotnet-runtime", + "ms-dotnettools.csdevkit", + "alexcvzz.vscode-sqlite", + "streetsidesoftware.code-spell-checker", + "eamodio.gitlens", + "redhat.vscode-xml" + ] + } + }, "features": { "ghcr.io/devcontainers/features/dotnet:2": { "version": "none", - "dotnetRuntimeVersions": "9.0", - "aspNetCoreRuntimeVersions": "9.0" + "dotnetRuntimeVersions": "10.0", + "aspNetCoreRuntimeVersions": "10.0" }, "ghcr.io/devcontainers-extra/features/apt-packages:1": { "preserve_apt_list": false, From 5656df4339cd9c764a6ddc30325f8ce4a589b3de Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:45:38 +0100 Subject: [PATCH 209/390] Update dependency z440.atl.core to 7.11.0 (#16199) 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 ea4436489a..668b601090 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> <PackageVersion Include="System.Text.Json" Version="10.0.2" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.10.0" /> + <PackageVersion Include="z440.atl.core" Version="7.11.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> From 558b31f3861ce6de3bbf7991fd125dd43c26bc69 Mon Sep 17 00:00:00 2001 From: Vincenzo Reale <smart2128vr@gmail.com> Date: Mon, 9 Feb 2026 05:07:59 -0500 Subject: [PATCH 210/390] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index c2974704be..d8eb8bc9e6 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -3,7 +3,7 @@ "AppDeviceValues": "App: {0}, Dispositivo: {1}", "Application": "Applicazione", "Artists": "Artisti", - "AuthenticationSucceededWithUserName": "{0} autenticato con successo", + "AuthenticationSucceededWithUserName": "{0} autenticato correttamente", "Books": "Libri", "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}", "Channels": "Canali", @@ -11,7 +11,7 @@ "Collections": "Collezioni", "DeviceOfflineWithName": "{0} si è disconnesso", "DeviceOnlineWithName": "{0} è connesso", - "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}", + "FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}", "Favorites": "Preferiti", "Folders": "Cartelle", "Genres": "Generi", From 01264c10a684768f9c8473cf769938522d3cc5d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Reale <smart2128vr@gmail.com> Date: Mon, 9 Feb 2026 05:11:00 -0500 Subject: [PATCH 211/390] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index d8eb8bc9e6..41a829757a 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -17,7 +17,7 @@ "Genres": "Generi", "HeaderAlbumArtists": "Artisti dell'album", "HeaderContinueWatching": "Continua a guardare", - "HeaderFavoriteAlbums": "Album Preferiti", + "HeaderFavoriteAlbums": "Album preferiti", "HeaderFavoriteArtists": "Artisti Preferiti", "HeaderFavoriteEpisodes": "Episodi Preferiti", "HeaderFavoriteShows": "Serie TV Preferite", From a0bf5199ba54161a616a697b50d3a0103e803ef6 Mon Sep 17 00:00:00 2001 From: Vincenzo Reale <smart2128vr@gmail.com> Date: Mon, 9 Feb 2026 05:42:16 -0500 Subject: [PATCH 212/390] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- .../Localization/Core/it.json | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 41a829757a..ff60e61274 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -18,29 +18,29 @@ "HeaderAlbumArtists": "Artisti dell'album", "HeaderContinueWatching": "Continua a guardare", "HeaderFavoriteAlbums": "Album preferiti", - "HeaderFavoriteArtists": "Artisti Preferiti", - "HeaderFavoriteEpisodes": "Episodi Preferiti", - "HeaderFavoriteShows": "Serie TV Preferite", - "HeaderFavoriteSongs": "Brani Preferiti", + "HeaderFavoriteArtists": "Artisti preferiti", + "HeaderFavoriteEpisodes": "Episodi preferiti", + "HeaderFavoriteShows": "Serie TV preferite", + "HeaderFavoriteSongs": "Brani preferiti", "HeaderLiveTV": "Diretta TV", "HeaderNextUp": "Prossimo", - "HeaderRecordingGroups": "Gruppi di Registrazione", - "HomeVideos": "Video Personali", + "HeaderRecordingGroups": "Gruppi di registrazione", + "HomeVideos": "Video personali", "Inherit": "Eredita", "ItemAddedWithName": "{0} è stato aggiunto alla libreria", "ItemRemovedWithName": "{0} è stato rimosso dalla libreria", "LabelIpAddressValue": "Indirizzo IP: {0}", "LabelRunningTimeValue": "Durata: {0}", "Latest": "Novità", - "MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato", + "MessageApplicationUpdated": "Jellyfin Server è stato aggiornato", "MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata", "MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata", "MixedContent": "Contenuto misto", "Movies": "Film", "Music": "Musica", - "MusicVideos": "Video Musicali", - "NameInstallFailed": "{0} installazione fallita", + "MusicVideos": "Video musicali", + "NameInstallFailed": "{0} installazione non riuscita", "NameSeasonNumber": "Stagione {0}", "NameSeasonUnknown": "Stagione sconosciuta", "NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.", @@ -49,37 +49,37 @@ "NotificationOptionAudioPlayback": "La riproduzione audio è iniziata", "NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta", "NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata", - "NotificationOptionInstallationFailed": "Installazione fallita", + "NotificationOptionInstallationFailed": "Installazione non riuscita", "NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto", "NotificationOptionPluginError": "Errore del plugin", "NotificationOptionPluginInstalled": "Plugin installato", "NotificationOptionPluginUninstalled": "Plugin disinstallato", "NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato", "NotificationOptionServerRestartRequired": "Riavvio del server necessario", - "NotificationOptionTaskFailed": "Operazione pianificata fallita", + "NotificationOptionTaskFailed": "Operazione pianificata non riuscita", "NotificationOptionUserLockedOut": "Utente bloccato", "NotificationOptionVideoPlayback": "Riproduzione video iniziata", "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta", "Photos": "Foto", "Playlists": "Playlist", "Plugin": "Plugin", - "PluginInstalledWithName": "{0} è stato Installato", + "PluginInstalledWithName": "{0} è stato installato", "PluginUninstalledWithName": "{0} è stato disinstallato", "PluginUpdatedWithName": "{0} è stato aggiornato", "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} fallito", + "ScheduledTaskFailedWithName": "{0} non riuscito", "ScheduledTaskStartedWithName": "{0} avviato", "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato", "Shows": "Serie TV", "Songs": "Brani", - "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", + "StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.", "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", "Sync": "Sincronizza", "System": "Sistema", "TvShows": "Serie TV", "User": "Utente", "UserCreatedWithName": "L'utente {0} è stato creato", - "UserDeletedWithName": "L'utente {0} è stato rimosso", + "UserDeletedWithName": "L'utente {0} è stato eliminato", "UserDownloadingItemWithValues": "{0} sta scaricando {1}", "UserLockedOutWithName": "L'utente {0} è stato bloccato", "UserOfflineFromDevice": "{0} si è disconnesso da {1}", @@ -115,7 +115,7 @@ "TasksMaintenanceCategory": "Manutenzione", "TaskCleanActivityLog": "Attività di Registro Completate", "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.", - "Undefined": "Non Definito", + "Undefined": "Non specificato", "Forced": "Forzato", "Default": "Predefinito", "TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.", @@ -123,7 +123,7 @@ "TaskKeyframeExtractor": "Estrattore di Keyframe", "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.", "External": "Esterno", - "HearingImpaired": "Non Udenti", + "HearingImpaired": "Non udenti", "TaskRefreshTrickplayImages": "Genera immagini Trickplay", "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.", "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist", From 6b8400cc3da0095fbce4f3f481464244b3ccf608 Mon Sep 17 00:00:00 2001 From: Vincenzo Reale <smart2128vr@gmail.com> Date: Mon, 9 Feb 2026 06:31:26 -0500 Subject: [PATCH 213/390] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index ff60e61274..f0c4b50270 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -61,7 +61,7 @@ "NotificationOptionVideoPlayback": "Riproduzione video iniziata", "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta", "Photos": "Foto", - "Playlists": "Playlist", + "Playlists": "Scalette", "Plugin": "Plugin", "PluginInstalledWithName": "{0} è stato installato", "PluginUninstalledWithName": "{0} è stato disinstallato", @@ -114,20 +114,20 @@ "TasksLibraryCategory": "Libreria", "TasksMaintenanceCategory": "Manutenzione", "TaskCleanActivityLog": "Attività di Registro Completate", - "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell’età configurata.", + "TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.", "Undefined": "Non specificato", "Forced": "Forzato", "Default": "Predefinito", "TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.", "TaskOptimizeDatabase": "Ottimizza database", "TaskKeyframeExtractor": "Estrattore di Keyframe", - "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.", + "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori scalette HLS. Questa procedura potrebbe richiedere molto tempo.", "External": "Esterno", "HearingImpaired": "Non udenti", "TaskRefreshTrickplayImages": "Genera immagini Trickplay", "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.", - "TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist", - "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.", + "TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette", + "TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.", "TaskAudioNormalization": "Normalizzazione dell'audio", "TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.", "TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni", From de36952f5378d5af28aa9edc660f47cd47a2f218 Mon Sep 17 00:00:00 2001 From: francescbassas <francesc@babu.cat> Date: Mon, 9 Feb 2026 16:28:49 -0500 Subject: [PATCH 214/390] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 82cc1857b7..1e7279be83 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -104,7 +104,7 @@ "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.", "TaskCleanLogs": "Neteja dels registres", "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.", - "TaskRefreshLibrary": "Escaneig de les mediateques", + "TaskRefreshLibrary": "Escaneja la mediateca", "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.", "TaskRefreshChapterImages": "Extracció de les imatges dels capítols", "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.", From 5eaaad660dfb0f6d12cf6dde8d50f25b08b24175 Mon Sep 17 00:00:00 2001 From: Pavel Miniutka <pavel.miniutka@gmail.com> Date: Tue, 10 Feb 2026 04:34:35 -0500 Subject: [PATCH 215/390] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 3d598c491f..cb11cc0894 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -3,7 +3,7 @@ "Playlists": "Плэй-лісты", "Latest": "Апошняе", "LabelIpAddressValue": "IP-адрас: {0}", - "ItemAddedWithName": "{0} даданы ў бібліятэку", + "ItemAddedWithName": "{0} дададзены ў бібліятэку", "MessageApplicationUpdated": "Сервер Jellyfin абноўлены", "NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана", "PluginInstalledWithName": "{0} быў усталяваны", @@ -14,7 +14,7 @@ "Channels": "Каналы", "ChapterNameValue": "Раздзел {0}", "Collections": "Калекцыі", - "Default": "Па змаўчанні", + "Default": "Прадвызначана", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", "Folders": "Папкі", "Favorites": "Абранае", @@ -81,8 +81,8 @@ "NotificationOptionInstallationFailed": "Збой усталёўкі", "NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.", "NotificationOptionCameraImageUploaded": "Выява камеры запампавана", - "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена", - "NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося", + "NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена", + "NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося", "NotificationOptionNewLibraryContent": "Дададзены новы кантэнт", "NotificationOptionPluginError": "Збой плагіна", "NotificationOptionPluginUninstalled": "Плагін выдалены", From 5aad260767161fc4a134df270fac04e146cc6142 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:05:17 +0000 Subject: [PATCH 216/390] Update dependency dotnet-ef to v10.0.3 --- .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 1d65527d98..302ac67b6f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.2", + "version": "10.0.3", "commands": [ "dotnet-ef" ] From 6fff4a7bfa6149e5f4f1e82d8bf349337fef0723 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:05:25 +0000 Subject: [PATCH 217/390] Update Microsoft to 10.0.3 --- Directory.Packages.props | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 668b601090..c5ffc1c51f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,27 +26,27 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> @@ -77,7 +77,7 @@ <PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> - <PackageVersion Include="System.Text.Json" Version="10.0.2" /> + <PackageVersion Include="System.Text.Json" Version="10.0.3" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> From 1c2f08bc173fca586484ece49326d477622ac0bf Mon Sep 17 00:00:00 2001 From: tyage <namatyage@gmail.com> Date: Thu, 12 Feb 2026 00:23:44 +0900 Subject: [PATCH 218/390] 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*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](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|\[.*\])([ _\,\.\(\)\[\]\-]|$)", - @"^(?<cleaned>.+?)(\[.*\])", + @"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](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*(?<cleaned>.+?)((\s*\[[^\]]+\]\s*)+)(\.[^\s]+)?$", @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)", @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)", @"^\s*(?<cleaned>.+?)\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 18a1cd388a0820bda453c29e5663e3847e99cbd9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:13:58 +0000 Subject: [PATCH 219/390] Update dependency AsyncKeyedLock to 8.0.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 668b601090..250b98854b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ </PropertyGroup> <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.--> <ItemGroup Label="Package Dependencies"> - <PackageVersion Include="AsyncKeyedLock" Version="8.0.1" /> + <PackageVersion Include="AsyncKeyedLock" Version="8.0.2" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> <PackageVersion Include="AutoFixture" Version="4.18.1" /> From e73ebc9741becd4acc87967a5ba5c8864d2f22ef Mon Sep 17 00:00:00 2001 From: DerMaddis <eulermat16@gmail.com> Date: Fri, 13 Feb 2026 12:03:58 +0100 Subject: [PATCH 220/390] TmdbSeriesProvider: Set ProductionYear in SearchResult mappers --- MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 93042c3252..1f13c10527 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -141,6 +141,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV } remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); + remoteResult.ProductionYear = series.FirstAirDate?.Year; return remoteResult; } @@ -157,6 +158,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime(); + remoteResult.ProductionYear = series.FirstAirDate?.Year; return remoteResult; } From f685a65241995b59d5ed584eb178ef88bc5cb9cd Mon Sep 17 00:00:00 2001 From: DerMaddis <eulermat16@gmail.com> Date: Fri, 13 Feb 2026 12:11:13 +0100 Subject: [PATCH 221/390] add to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1770db60be..cb7d3fbbc4 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -287,3 +287,4 @@ - [Martin Reuter](https://github.com/reuterma24) - [Michael McElroy](https://github.com/mcmcelro) - [Soumyadip Auddy](https://github.com/SoumyadipAuddy) + - [DerMaddis](https://github.com/dermaddis) From a1117a1fbd9d7a26c4a792e9729394a52745a8f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:33:57 +0000 Subject: [PATCH 222/390] Update github/codeql-action action to v4.32.3 --- .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 9072fa9f9e..10fb775fea 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@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 From 2757c18312943cbc17ce01b100f73b411f596a98 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Fri, 13 Feb 2026 11:52:10 -0500 Subject: [PATCH 223/390] Fix episodes appearing in Season Unknown incorrectly and prevent unnecessary virtual season creation --- MediaBrowser.Controller/Entities/TV/Series.cs | 3 +- .../TV/SeriesMetadataService.cs | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 6396631f99..b3956c8233 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -451,7 +451,8 @@ namespace MediaBrowser.Controller.Entities.TV if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual) { - return true; + var episodeSeason = episodeItem.Season; + return episodeSeason is null || episodeSeason.LocationType == LocationType.Virtual; } var season = episodeItem.Season; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index c3a6ddd6ae..61a31fbfd6 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -201,6 +202,26 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> false); } + private static bool NeedsVirtualSeason(Episode episode, HashSet<Guid> physicalSeasonIds, HashSet<string> physicalSeasonPaths) + { + // Episode has a known season number, needs a season + if (episode.ParentIndexNumber.HasValue) + { + return true; + } + + // Not yet processed + if (episode.SeasonId.IsEmpty()) + { + return false; + } + + // Episode has been processed, only needs a virtual season if it isn't + // already linked to a known physical season by ID or path + return !physicalSeasonIds.Contains(episode.SeasonId) + && !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty); + } + /// <summary> /// Creates seasons for all episodes if they don't exist. /// If no season number can be determined, a dummy season will be created. @@ -212,8 +233,20 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var seasons = seriesChildren.OfType<Season>().ToList(); + + var physicalSeasonIds = seasons + .Where(e => e.LocationType != LocationType.Virtual) + .Select(e => e.Id) + .ToHashSet(); + + var physicalSeasonPathSet = seasons + .Where(e => e.LocationType != LocationType.Virtual && !string.IsNullOrEmpty(e.Path)) + .Select(e => e.Path) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var uniqueSeasonNumbers = seriesChildren .OfType<Episode>() + .Where(e => NeedsVirtualSeason(e, physicalSeasonIds, physicalSeasonPathSet)) .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) .Distinct(); From 48e456903e94fbe3e1008bcf9f3d155a4f26d5a5 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Fri, 13 Feb 2026 16:28:22 -0500 Subject: [PATCH 224/390] Apply review feedback --- MediaBrowser.Controller/Entities/TV/Series.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index b3956c8233..6a26ecaebe 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -451,8 +451,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual) { - var episodeSeason = episodeItem.Season; - return episodeSeason is null || episodeSeason.LocationType == LocationType.Virtual; + return episodeItem.Season is null or { LocationType: LocationType.Virtual }; } var season = episodeItem.Season; From dce91cf8c8d01135c10058614551f2f7f70a633c Mon Sep 17 00:00:00 2001 From: saltpi <endpne@gmail.com> Date: Sat, 14 Feb 2026 05:57:23 -0500 Subject: [PATCH 225/390] Backport pull request #16116 from jellyfin/release-10.11.z Fix TMDB image URLs missing size parameter Original-merge: caa05c1bf2c55053a65091886f0ddf78865fc72c Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index fedf345988..abaca65ff3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -518,7 +518,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return null; } - return _tmDbClient.GetImageUrl(size, path, true).ToString(); + // Use "original" as default size if size is null or empty to prevent malformed URLs + var imageSize = string.IsNullOrEmpty(size) ? "original" : size; + + return _tmDbClient.GetImageUrl(imageSize, path, true).ToString(); } /// <summary> From d8543351e256fef6f71ab2a4a45fcc50cfc4084c Mon Sep 17 00:00:00 2001 From: dfederm <david.federman@outlook.com> Date: Sat, 14 Feb 2026 05:57:24 -0500 Subject: [PATCH 226/390] Backport pull request #16226 from jellyfin/release-10.11.z Deduplicate provider IDs during MigrateLibraryDb migration Original-merge: 58c330b63d5e373ed1d9260a75d837922ca68830 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 4b1e53a355..70761fa7db 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1163,7 +1163,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine Item = null!, ProviderId = e[0], ProviderValue = string.Join('|', e.Skip(1)) - }).ToArray(); + }) + .DistinctBy(e => e.ProviderId) + .ToArray(); } if (reader.TryGetString(index++, out var imageInfos)) From a37e83d448598cfd06fee7f52ee7130248d6bac3 Mon Sep 17 00:00:00 2001 From: dfederm <david.federman@outlook.com> Date: Sat, 14 Feb 2026 05:57:25 -0500 Subject: [PATCH 227/390] Backport pull request #16227 from jellyfin/release-10.11.z Reattach user data after item removal during library scan Original-merge: be712956932a9337f0706fd8ef68eb53feb3f4ff Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/Folder.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d2a3290c47..2ecb6cbdff 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities // That's all the new and changed ones - now see if any have been removed and need cleanup var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var shouldRemove = !IsRoot || allowRemoveRoot; + var actuallyRemoved = new List<BaseItem>(); // If it's an AggregateFolder, don't remove if (shouldRemove && itemsRemoved.Count > 0) { @@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities { Logger.LogDebug("Removed item: {Path}", item.Path); + actuallyRemoved.Add(item); item.SetParent(null); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); } @@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities { LibraryManager.CreateItems(newItems, this, cancellationToken); } + + // After removing items, reattach any detached user data to remaining children + // that share the same user data keys (eg. same episode replaced with a new file). + if (actuallyRemoved.Count > 0) + { + var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet(); + foreach (var child in validChildren) + { + if (child.GetUserDataKeys().Any(removedKeys.Contains)) + { + await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); + } + } + } } else { From 074aa7e6394ede1998c53bd9c1528c0c5dfddcd3 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sat, 14 Feb 2026 05:57:26 -0500 Subject: [PATCH 228/390] Backport pull request #16231 from jellyfin/release-10.11.z Skip image checks for empty folders Original-merge: 8cd3090ceedbefd680b26dc28266ae9a6e5d652a Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Images/BaseDynamicImageProvider.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 4874eca8e6..996cd1b3ca 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images { var image = item.GetImageInfo(type, 0); - if (image is not null) + if (image is null) { - if (!image.IsLocalFile) - { - return false; - } + return GetItemsWithImages(item).Count is not 0; + } - if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) - { - return false; - } + if (!image.IsLocalFile) + { + return false; + } - if (!HasChangedByDate(item, image)) - { - return false; - } + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) + { + return false; + } + + if (!HasChangedByDate(item, image)) + { + return false; } return true; From ca6d499680f9fbb369844a11eb0e0213b66bb00b Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Sat, 14 Feb 2026 12:06:18 +0100 Subject: [PATCH 229/390] Update Jellyfin.Api/Controllers/PlaylistsController.cs --- Jellyfin.Api/Controllers/PlaylistsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 1076d1eb06..1940895dd4 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -359,7 +359,7 @@ public class PlaylistsController : BaseJellyfinApiController /// </summary> /// <param name="playlistId">The playlist id.</param> /// <param name="ids">Item id, comma delimited.</param> - /// <param name="position">Optional. 0-based index where to place the items or at the end if null.</param> + /// <param name="position">Optional. 0-based index where to place the items or at the end if <c>null</c>.</param> /// <param name="userId">The userId.</param> /// <response code="204">Items added to playlist.</response> /// <response code="403">Access forbidden.</response> From 106f33227a20f1785b7a6c59ef6d4b68a5a8a880 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Sat, 14 Feb 2026 00:32:07 -0500 Subject: [PATCH 230/390] 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<MusicArtist>(); - - if (artist is not null) - { - return artist.GetImages(imageType).ElementAtOrDefault(imageIndex); - } - } - return GetImages(imageType) .ElementAtOrDefault(imageIndex); } From a56aa2dd5370c09cc36f87f43f75f27fc6e3a5e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:37:05 +0000 Subject: [PATCH 231/390] Update dependency coverlet.collector to v8 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f4c5302ab..0cb0172382 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> - <PackageVersion Include="coverlet.collector" Version="6.0.4" /> + <PackageVersion Include="coverlet.collector" Version="8.0.0" /> <PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> From 04ffbe5e9a16723e7cb63392aee5c45c5e9e367f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:49:18 +0000 Subject: [PATCH 232/390] Update dependency TMDbLib to v3 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2f4c5302ab..167b956b68 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,7 +80,7 @@ <PackageVersion Include="System.Text.Json" Version="10.0.3" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> - <PackageVersion Include="TMDbLib" Version="2.3.0" /> + <PackageVersion Include="TMDbLib" Version="3.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> From b346370dfcaa7a16cc9e292ba1c1b7e43e945783 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sun, 15 Feb 2026 11:28:42 +0100 Subject: [PATCH 233/390] Fix build --- Emby.Naming/TV/TvParserHelpers.cs | 2 +- .../Providers/RemoteSearchResult.cs | 11 ++-- .../Plugins/Tmdb/Api/TmdbController.cs | 2 +- .../Tmdb/BoxSets/TmdbBoxSetImageProvider.cs | 13 +++- .../Tmdb/BoxSets/TmdbBoxSetProvider.cs | 6 +- .../Tmdb/Movies/TmdbMovieImageProvider.cs | 23 +++++-- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 35 ++++++---- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 10 ++- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 2 +- .../Tmdb/TV/TmdbSeriesImageProvider.cs | 19 ++++-- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 36 ++++++---- .../Plugins/Tmdb/TmdbClientManager.cs | 65 +++++++++++-------- .../Plugins/Tmdb/TmdbUtils.cs | 28 ++++---- src/Jellyfin.Extensions/StringExtensions.cs | 2 +- 14 files changed, 165 insertions(+), 89 deletions(-) diff --git a/Emby.Naming/TV/TvParserHelpers.cs b/Emby.Naming/TV/TvParserHelpers.cs index 0299178582..706251f297 100644 --- a/Emby.Naming/TV/TvParserHelpers.cs +++ b/Emby.Naming/TV/TvParserHelpers.cs @@ -18,7 +18,7 @@ public static class TvParserHelpers /// <param name="status">The status string.</param> /// <param name="enumValue">The <see cref="SeriesStatus"/>.</param> /// <returns>Returns true if parsing was successful.</returns> - public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue) + public static bool TryParseSeriesStatus(string? status, out SeriesStatus? enumValue) { if (Enum.TryParse(status, true, out SeriesStatus seriesStatus)) { diff --git a/MediaBrowser.Model/Providers/RemoteSearchResult.cs b/MediaBrowser.Model/Providers/RemoteSearchResult.cs index a29e7ad1c5..7d3b5e4ab8 100644 --- a/MediaBrowser.Model/Providers/RemoteSearchResult.cs +++ b/MediaBrowser.Model/Providers/RemoteSearchResult.cs @@ -1,4 +1,3 @@ -#nullable disable #pragma warning disable CS1591 using System; @@ -19,7 +18,7 @@ namespace MediaBrowser.Model.Providers /// Gets or sets the name. /// </summary> /// <value>The name.</value> - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the provider ids. @@ -41,13 +40,13 @@ namespace MediaBrowser.Model.Providers public DateTime? PremiereDate { get; set; } - public string ImageUrl { get; set; } + public string? ImageUrl { get; set; } - public string SearchProviderName { get; set; } + public string? SearchProviderName { get; set; } - public string Overview { get; set; } + public string? Overview { get; set; } - public RemoteSearchResult AlbumArtist { get; set; } + public RemoteSearchResult? AlbumArtist { get; set; } public RemoteSearchResult[] Artists { get; set; } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs index 450ee2a337..3eacc4f0f0 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api /// <returns>The image portion of the TMDb client configuration.</returns> [HttpGet("ClientConfiguration")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ConfigImageTypes> TmdbClientConfiguration() + public async Task<ConfigImageTypes?> TmdbClientConfiguration() { return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index 02818a0e24..78be5804e3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -75,10 +75,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets var posters = collection.Images.Posters; var backdrops = collection.Images.Backdrops; - var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count); + var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0); - remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); - remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); + if (posters is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); + } + + if (backdrops is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); + } return remoteImages; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 34c9abae12..a7bba2d539 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -67,10 +67,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture)); - return new[] { result }; + return [result]; } var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false); + if (collectionSearchResults is null) + { + return []; + } var collections = new RemoteSearchResult[collectionSearchResults.Count]; for (var i = 0; i < collectionSearchResults.Count; i++) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs index fcc3574107..714c57d361 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (movieTmdbId <= 0) { - return Enumerable.Empty<RemoteImageInfo>(); + return []; } // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here @@ -89,17 +89,28 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (movie?.Images is null) { - return Enumerable.Empty<RemoteImageInfo>(); + return []; } var posters = movie.Images.Posters; var backdrops = movie.Images.Backdrops; var logos = movie.Images.Logos; - var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count); + var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0); - remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); - remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); - remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); + if (posters is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); + } + + if (backdrops is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); + } + + if (logos is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); + } return remoteImages; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 2beb34e43b..ff584ba1de 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -15,6 +15,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using TMDbLib.Objects.Find; +using TMDbLib.Objects.General; using TMDbLib.Objects.Search; namespace MediaBrowser.Providers.Plugins.Tmdb.Movies @@ -84,7 +85,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture)); remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId); - return new[] { remoteResult }; + return [remoteResult]; } } @@ -118,6 +119,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies .ConfigureAwait(false); } + if (movieResults is null) + { + return []; + } + var len = movieResults.Count; var remoteSearchResults = new RemoteSearchResult[len]; for (var i = 0; i < len; i++) @@ -158,7 +164,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - if (searchResults.Count > 0) + if (searchResults?.Count > 0) { tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); } @@ -167,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId)) { var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - if (movieResultFromImdbId?.MovieResults.Count > 0) + if (movieResultFromImdbId?.MovieResults?.Count > 0) { tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture); } @@ -193,7 +199,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies OriginalTitle = movieResult.OriginalTitle, Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture), Tagline = movieResult.Tagline, - ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray() + ProductionLocations = movieResult.ProductionCountries?.Select(pc => pc.Name).ToArray() ?? Array.Empty<string>() }; var metadataResult = new MetadataResult<Movie> { @@ -218,14 +224,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)); - if (ourRelease is not null) + if (ourRelease?.Certification is not null) { - movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification); + movie.OfficialRating = TmdbUtils.BuildParentalRating(info.MetadataCountryCode, ourRelease.Certification); } else { var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); - if (usRelease is not null) + if (usRelease?.Certification is not null) { movie.OfficialRating = usRelease.Certification; } @@ -242,16 +248,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var genres = movieResult.Genres; - foreach (var genre in genres.Select(g => g.Name).Trimmed()) + if (genres is not null) { - movie.AddGenre(genre); + foreach (var genre in genres.Select(g => g.Name).Trimmed()) + { + movie.AddGenre(genre); + } } if (movieResult.Keywords?.Keywords is not null) { - for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++) + foreach (var keyword in movieResult.Keywords.Keywords) { - movie.AddTag(movieResult.Keywords.Keywords[i].Name); + var name = keyword.Name; + if (!string.IsNullOrWhiteSpace(name)) + { + movie.AddTag(name); + } } } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 4b32d0f6bf..64ab98b262 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -56,13 +56,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People } result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture)); - result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId); + result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds?.ImdbId); - return new[] { result }; + return [result]; } } var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false); + if (personSearchResult is null) + { + return []; + } var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count]; for (var i = 0; i < personSearchResult.Count; i++) @@ -91,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People if (personTmdbId <= 0) { var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false); - if (personSearchResults.Count > 0) + if (personSearchResults?.Count > 0) { personTmdbId = personSearchResults[0].Id; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 0905a3bdcb..1eb522137d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV result.Item.Name = seasonResult.Name; } - result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId); + result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds?.TvdbId); // TODO why was this disabled? var credits = seasonResult.Credits; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs index 5cba84dcb3..f2e7d0c6e4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -79,11 +79,22 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var posters = series.Images.Posters; var backdrops = series.Images.Backdrops; var logos = series.Images.Logos; - var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count); + var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0); - remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); - remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); - remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); + if (posters is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language)); + } + + if (backdrops is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language)); + } + + if (logos is not null) + { + remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language)); + } return remoteImages; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 1f13c10527..7e36c1e204 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -112,6 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken) .ConfigureAwait(false); + if (tvSearchResults is null) + { + return []; + } var remoteResults = new RemoteSearchResult[tvSearchResults.Count]; for (var i = 0; i < tvSearchResults.Count; i++) @@ -176,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId)) { var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - if (searchResult?.TvResults.Count > 0) + if (searchResult?.TvResults?.Count > 0) { tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); } @@ -185,7 +189,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId)) { var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false); - if (searchResult?.TvResults.Count > 0) + if (searchResult?.TvResults?.Count > 0) { tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture); } @@ -200,7 +204,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var cleanedName = TmdbUtils.CleanName(parsedName.Name); var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false); - if (searchResults.Count > 0) + if (searchResults?.Count > 0) { tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture); } @@ -264,15 +268,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV if (seriesResult.Keywords?.Results is not null) { - for (var i = 0; i < seriesResult.Keywords.Results.Count; i++) + foreach (var result in seriesResult.Keywords.Results) { - series.AddTag(seriesResult.Keywords.Results[i].Name); + var name = result.Name; + if (!string.IsNullOrWhiteSpace(name)) + { + series.AddTag(name); + } } } series.HomePageUrl = seriesResult.Homepage; - series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); + series.RunTimeTicks = seriesResult.EpisodeRunTime?.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus)) { @@ -291,21 +299,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId); } - var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>(); + var contentRatings = seriesResult.ContentRatings?.Results ?? new List<ContentRating>(); var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); var minimumRelease = contentRatings.FirstOrDefault(); - if (ourRelease is not null) + if (ourRelease?.Rating is not null) { - series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating); + series.OfficialRating = TmdbUtils.BuildParentalRating(preferredCountryCode, ourRelease.Rating); } - else if (usRelease is not null) + else if (usRelease?.Rating is not null) { series.OfficialRating = usRelease.Rating; } - else if (minimumRelease is not null) + else if (minimumRelease?.Rating is not null) { series.OfficialRating = minimumRelease.Rating; } @@ -350,7 +358,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV Role = actor.Character?.Trim() ?? string.Empty, Type = PersonKind.Actor, SortOrder = actor.Order, - ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath) + // NOTE: Null values are filtered out above + ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!) }; if (actor.Id > 0) @@ -391,7 +400,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV Name = crewMember.Name.Trim(), Role = crewMember.Job?.Trim() ?? string.Empty, Type = entry.PersonType, - ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath) + // NOTE: Null values are filtered out above + ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!) }; if (crewMember.Id > 0) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index abaca65ff3..274db347ba 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; @@ -195,7 +194,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb await EnsureClientConfigAsync().ConfigureAwait(false); var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false); - var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id; + var episodeGroupId = series?.EpisodeGroups?.Results?.Find(g => g.Type == groupType)?.Id; if (episodeGroupId is null) { @@ -263,7 +262,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv episode information or null if not found.</returns> - public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken) + public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, long episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken) { var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}"; if (_memoryCache.TryGetValue(key, out TvEpisode? episode)) @@ -276,9 +275,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false); if (group is not null) { - var season = group.Groups.Find(s => s.Order == seasonNumber); + var season = group.Groups?.Find(s => s.Order == seasonNumber); // Episode order starts at 0 - var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1); + var ep = season?.Episodes?.Find(e => e.Order == episodeNumber - 1); if (ep is not null) { seasonNumber = ep.SeasonNumber; @@ -382,7 +381,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="year">The year the tv show first aired.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb tv show information.</returns> - public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default) + public async Task<IReadOnlyList<SearchTv>?> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default) { var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null) @@ -396,12 +395,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken) .ConfigureAwait(false); - if (searchResults.Results.Count > 0) + if (searchResults?.Results?.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } - return searchResults.Results; + return searchResults?.Results; } /// <summary> @@ -410,7 +409,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="name">The name of the person.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb person information.</returns> - public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken) + public async Task<IReadOnlyList<SearchPerson>?> SearchPersonAsync(string name, CancellationToken cancellationToken) { var key = $"searchperson-{name}"; if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null) @@ -424,12 +423,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken) .ConfigureAwait(false); - if (searchResults.Results.Count > 0) + if (searchResults?.Results?.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } - return searchResults.Results; + return searchResults?.Results; } /// <summary> @@ -439,7 +438,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="language">The movie's language.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb movie information.</returns> - public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) + public Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, string language, CancellationToken cancellationToken) { return SearchMovieAsync(name, 0, language, null, cancellationToken); } @@ -453,7 +452,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb movie information.</returns> - public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken) + public async Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken) { var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null) @@ -467,12 +466,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken) .ConfigureAwait(false); - if (searchResults.Results.Count > 0) + if (searchResults?.Results?.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } - return searchResults.Results; + return searchResults?.Results; } /// <summary> @@ -483,7 +482,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="countryCode">The country code, ISO 3166-1.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The TMDb collection information.</returns> - public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken) + public async Task<IReadOnlyList<SearchCollection>?> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken) { var key = $"collectionsearch-{name}-{language}"; if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null) @@ -497,12 +496,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken) .ConfigureAwait(false); - if (searchResults.Results.Count > 0) + if (searchResults?.Results?.Count > 0) { _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours)); } - return searchResults.Results; + return searchResults?.Results; } /// <summary> @@ -511,7 +510,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="size">The image size to fetch.</param> /// <param name="path">The relative URL of the image.</param> /// <returns>The absolute URL.</returns> - private string? GetUrl(string? size, string path) + private string? GetUrl(string? size, string? path) { if (string.IsNullOrEmpty(path)) { @@ -529,7 +528,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="posterPath">The relative URL of the poster.</param> /// <returns>The absolute URL.</returns> - public string? GetPosterUrl(string posterPath) + public string? GetPosterUrl(string? posterPath) { return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath); } @@ -539,7 +538,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// </summary> /// <param name="actorProfilePath">The relative URL of the profile image.</param> /// <returns>The absolute URL.</returns> - public string? GetProfileUrl(string actorProfilePath) + public string? GetProfileUrl(string? actorProfilePath) { return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath); } @@ -642,30 +641,44 @@ namespace MediaBrowser.Providers.Plugins.Tmdb private static void ValidatePreferences(TMDbConfig config) { var imageConfig = config.Images; + if (imageConfig is null) + { + return; + } var pluginConfig = Plugin.Instance.Configuration; - if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize)) + if (imageConfig.PosterSizes is not null + && pluginConfig.PosterSize is not null + && !imageConfig.PosterSizes.Contains(pluginConfig.PosterSize)) { pluginConfig.PosterSize = imageConfig.PosterSizes[^1]; } - if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize)) + if (imageConfig.BackdropSizes is not null + && pluginConfig.BackdropSize is not null + && !imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize)) { pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1]; } - if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize)) + if (imageConfig.LogoSizes is not null + && pluginConfig.LogoSize is not null + && !imageConfig.LogoSizes.Contains(pluginConfig.LogoSize)) { pluginConfig.LogoSize = imageConfig.LogoSizes[^1]; } - if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize)) + if (imageConfig.ProfileSizes is not null + && pluginConfig.ProfileSize is not null + && !imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize)) { pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1]; } - if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize)) + if (imageConfig.StillSizes is not null + && pluginConfig.StillSize is not null + && !imageConfig.StillSizes.Contains(pluginConfig.StillSize)) { pluginConfig.StillSize = imageConfig.StillSizes[^1]; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 0944b557e9..39c0497bed 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -69,20 +69,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <returns>The Jellyfin person type.</returns> public static PersonKind MapCrewToPersonType(Crew crew) { - if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase) - && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(crew.Department, "directing", StringComparison.OrdinalIgnoreCase) + && string.Equals(crew.Job, "director", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Director; } - if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) - && crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(crew.Department, "production", StringComparison.OrdinalIgnoreCase) + && string.Equals(crew.Job, "producer", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Producer; } - if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase) - && (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase))) + if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase) + && (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase))) { return PersonKind.Writer; } @@ -97,9 +97,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <returns>A boolean indicating whether the video is a trailer.</returns> public static bool IsTrailerType(Video video) { - return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase) - && (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) - || video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase)); + return string.Equals(video.Site, "youtube", StringComparison.OrdinalIgnoreCase) + && (string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase) + || string.Equals(video.Type, "teaser", StringComparison.OrdinalIgnoreCase)); } /// <summary> @@ -177,10 +177,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <param name="imageLanguage">The image's actual language code.</param> /// <param name="requestLanguage">The requested language code.</param> /// <returns>The language code.</returns> - public static string AdjustImageLanguage(string imageLanguage, string requestLanguage) + public static string AdjustImageLanguage(string? imageLanguage, string requestLanguage) { - if (!string.IsNullOrEmpty(imageLanguage) - && !string.IsNullOrEmpty(requestLanguage) + if (string.IsNullOrEmpty(imageLanguage)) + { + return string.Empty; + } + + if (!string.IsNullOrEmpty(requestLanguage) && requestLanguage.Length > 2 && imageLanguage.Length == 2 && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase)) diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index 60df47113a..c7e8319f59 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -131,7 +131,7 @@ namespace Jellyfin.Extensions /// </summary> /// <param name="values">The enumerable of strings to trim.</param> /// <returns>The enumeration of trimmed strings.</returns> - public static IEnumerable<string> Trimmed(this IEnumerable<string> values) + public static IEnumerable<string> Trimmed(this IEnumerable<string?> values) { return values.Select(i => (i ?? string.Empty).Trim()); } From daf88a5ca26f7f54d8258d10d59035c4b4c2ab92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:49:00 +0000 Subject: [PATCH 234/390] Update actions/stale action to v10.2.0 --- .github/workflows/issue-stale.yml | 2 +- .github/workflows/pull-request-stale.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index cb535297e0..339fcf569e 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index 0d74e643e2..e114276c28 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true From 37b50fe13c689e8fb89288da12f79bf7164e1194 Mon Sep 17 00:00:00 2001 From: Cosmin Dumitru <cosu@cosu.ro> Date: Wed, 18 Feb 2026 21:08:35 +0100 Subject: [PATCH 235/390] 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 <noreply@anthropic.com> --- 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<string, string?> streamOptions = new Dictionary<string, string?>(); 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 <Ghost_of_Stone@web.de> Date: Wed, 18 Feb 2026 22:39:49 +0100 Subject: [PATCH 236/390] 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 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.4.1" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.4" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" /> <PackageVersion Include="System.Text.Json" Version="10.0.3" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> 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<FileRequestFilter>(); c.OperationFilter<ParameterObsoleteFilter>(); c.DocumentFilter<AdditionalModelFilter>(); + c.DocumentFilter<SecuritySchemeReferenceFixupFilter>(); }) .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>()); } @@ -333,10 +331,10 @@ namespace Jellyfin.Server.Extensions options.MapType<Dictionary<ImageType, string>>(() => 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<Dictionary<string, string?>>(() => 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<Version>(() => 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; /// <summary> @@ -48,8 +47,8 @@ namespace Jellyfin.Server.Filters && t != typeof(WebSocketMessageInfo)) .ToList(); - var inboundWebSocketSchemas = new List<OpenApiSchema>(); - var inboundWebSocketDiscriminators = new Dictionary<string, string>(); + var inboundWebSocketSchemas = new List<IOpenApiSchema>(); + var inboundWebSocketDiscriminators = new Dictionary<string, OpenApiSchemaReference>(); foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t))) { var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.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<OpenApiSchema>(); - var outboundWebSocketDiscriminators = new Dictionary<string, string>(); + var outboundWebSocketSchemas = new List<IOpenApiSchema>(); + var outboundWebSocketDiscriminators = new Dictionary<string, OpenApiSchemaReference>(); foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t))) { var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.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<string, OpenApiSchema> + Properties = new Dictionary<string, IOpenApiSchema> { { "Data", new OpenApiSchema { - AllOf = - [ - new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate<object>) } } - ], + AllOf = new List<IOpenApiSchema> + { + new OpenApiSchemaReference(nameof(GroupUpdate<object>), null, null) + }, Description = "Group update data", - Nullable = false, } }, - { "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } }, + { "MessageId", new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid", Description = "Gets or sets the message id." } }, { "MessageType", new OpenApiSchema { - Enum = Enum.GetValues<SessionMessageType>().Select(type => new OpenApiString(type.ToString())).ToList<IOpenApiAny>(), - AllOf = - [ - new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } } - ], + Enum = Enum.GetValues<SessionMessageType>().Select(type => (JsonNode)JsonValue.Create(type.ToString())!).ToList(), + AllOf = new List<IOpenApiSchema> + { + new OpenApiSchemaReference(nameof(SessionMessageType), null, null) + }, Description = "The different kinds of messages that are used in the WebSocket api.", - Default = new OpenApiString(nameof(SessionMessageType.SyncPlayGroupUpdate)), + Default = JsonValue.Create(nameof(SessionMessageType.SyncPlayGroupUpdate)), ReadOnly = true } }, }, AdditionalPropertiesAllowed = false, - Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "SyncPlayGroupUpdateMessage" } }; context.SchemaRepository.AddDefinition("SyncPlayGroupUpdateMessage", syncPlayGroupUpdateMessageSchema); - outboundWebSocketSchemas.Add(syncPlayGroupUpdateMessageSchema); - outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayGroupUpdateMessageSchema.Reference.ReferenceV3; + var syncPlayRef = new OpenApiSchemaReference("SyncPlayGroupUpdateMessage", null, null); + outboundWebSocketSchemas.Add(syncPlayRef); + outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayRef; var outboundWebSocketMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible outbound websocket types", - Reference = new OpenApiReference - { - Id = nameof(OutboundWebSocketMessage), - Type = ReferenceType.Schema - }, OneOf = outboundWebSocketSchemas, Discriminator = new OpenApiDiscriminator { @@ -159,17 +153,12 @@ namespace Jellyfin.Server.Filters nameof(WebSocketMessage), new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the possible websocket types", - Reference = new OpenApiReference + OneOf = new List<IOpenApiSchema> { - Id = nameof(WebSocketMessage), - Type = ReferenceType.Schema - }, - OneOf = new[] - { - inboundWebSocketMessageSchema, - outboundWebSocketMessageSchema + new OpenApiSchemaReference(nameof(InboundWebSocketMessage), null, null), + new OpenApiSchemaReference(nameof(OutboundWebSocketMessage), null, null) } }); @@ -180,8 +169,8 @@ namespace Jellyfin.Server.Filters && t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>)) .ToList(); - var groupUpdateSchemas = new List<OpenApiSchema>(); - var groupUpdateDiscriminators = new Dictionary<string, string>(); + var groupUpdateSchemas = new List<IOpenApiSchema>(); + var groupUpdateDiscriminators = new Dictionary<string, OpenApiSchemaReference>(); foreach (var type in groupUpdateTypes) { var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate<object>.Type))?.GetCustomAttribute<DefaultValueAttribute>()?.Value; @@ -192,18 +181,16 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); groupUpdateSchemas.Add(schema); - groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3; + if (schema is OpenApiSchemaReference schemaRef) + { + groupUpdateDiscriminators[groupUpdateType.ToString()!] = schemaRef; + } } var groupUpdateSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible group update types", - Reference = new OpenApiReference - { - Id = nameof(GroupUpdate<object>), - Type = ReferenceType.Schema - }, OneOf = groupUpdateSchemas, Discriminator = new OpenApiDiscriminator { diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index 833b684444..fdc49a9840 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -48,7 +48,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider } /// <inheritdoc /> - public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null) + public OpenApiDocument GetSwagger(string documentName, string host, string basePath) { if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null) { diff --git a/Jellyfin.Server/Filters/FileRequestFilter.cs b/Jellyfin.Server/Filters/FileRequestFilter.cs index 86dbf7657e..3d5b1fdf1f 100644 --- a/Jellyfin.Server/Filters/FileRequestFilter.cs +++ b/Jellyfin.Server/Filters/FileRequestFilter.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -28,10 +28,11 @@ namespace Jellyfin.Server.Filters { Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "binary" } }; + body.Content ??= new System.Collections.Generic.Dictionary<string, OpenApiMediaType>(); foreach (var contentType in contentTypes) { body.Content.Add(contentType, mediaType); diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs index cd0acadf32..64aea62519 100644 --- a/Jellyfin.Server/Filters/FileResponseFilter.cs +++ b/Jellyfin.Server/Filters/FileResponseFilter.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Filters { Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "binary" } }; @@ -22,6 +22,11 @@ namespace Jellyfin.Server.Filters /// <inheritdoc /> public void Apply(OpenApiOperation operation, OperationFilterContext context) { + if (operation.Responses is null) + { + return; + } + foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata) { if (attribute is ProducesFileAttribute producesFileAttribute) @@ -31,7 +36,7 @@ namespace Jellyfin.Server.Filters .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal)); // Operation doesn't have a response. - if (response.Value is null) + if (response.Value?.Content is null) { continue; } diff --git a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs index 3e0b69d017..0c1f4197ce 100644 --- a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs +++ b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs @@ -1,5 +1,5 @@ using System; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters; public class FlagsEnumSchemaFilter : ISchemaFilter { /// <inheritdoc /> - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) { var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type); if (type is null || !type.IsEnum) @@ -29,11 +29,16 @@ public class FlagsEnumSchemaFilter : ISchemaFilter return; } + if (schema is not OpenApiSchema concreteSchema) + { + return; + } + if (context.MemberInfo is null) { // Processing the enum definition itself - ensure it's type "string" not "integer" - schema.Type = "string"; - schema.Format = null; + concreteSchema.Type = JsonSchemaType.String; + concreteSchema.Format = null; } else { @@ -43,11 +48,11 @@ public class FlagsEnumSchemaFilter : ISchemaFilter // Flags enums should be represented as arrays referencing the enum schema // since multiple values can be combined - schema.Type = "array"; - schema.Format = null; - schema.Enum = null; - schema.AllOf = null; - schema.Items = enumSchema; + concreteSchema.Type = JsonSchemaType.Array; + concreteSchema.Format = null; + concreteSchema.Enum = null; + concreteSchema.AllOf = null; + concreteSchema.Items = enumSchema; } } } diff --git a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs index eb9ad03c21..3dcf29d9c2 100644 --- a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs +++ b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json.Nodes; using Jellyfin.Data.Attributes; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters; public class IgnoreEnumSchemaFilter : ISchemaFilter { /// <inheritdoc /> - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) { if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false)) { @@ -25,18 +25,23 @@ public class IgnoreEnumSchemaFilter : ISchemaFilter return; } - var enumOpenApiStrings = new List<IOpenApiAny>(); + if (schema is not OpenApiSchema concreteSchema) + { + return; + } + + var enumOpenApiNodes = new List<JsonNode>(); foreach (var enumName in Enum.GetNames(type)) { var member = type.GetMember(enumName)[0]; if (!member.GetCustomAttributes<OpenApiIgnoreEnumAttribute>().Any()) { - enumOpenApiStrings.Add(new OpenApiString(enumName)); + enumOpenApiNodes.Add(JsonValue.Create(enumName)!); } } - schema.Enum = enumOpenApiStrings; + concreteSchema.Enum = enumOpenApiNodes; } } } diff --git a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs index 98a8dc0f18..90bca884b1 100644 --- a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs +++ b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -21,11 +21,17 @@ namespace Jellyfin.Server.Filters .OfType<ParameterObsoleteAttribute>() .Any()) { + if (operation.Parameters is null) + { + continue; + } + foreach (var parameter in operation.Parameters) { - if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal)) + if (parameter is OpenApiParameter concreteParam + && string.Equals(concreteParam.Name, parameterDescription.Name, StringComparison.Ordinal)) { - parameter.Deprecated = true; + concreteParam.Deprecated = true; break; } } diff --git a/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs index 8b7268513a..435f55496a 100644 --- a/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs +++ b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -8,12 +8,12 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { - operation.Responses.TryAdd( + operation.Responses?.TryAdd( "503", new OpenApiResponse { Description = "The server is currently starting or is temporarily not available.", - Headers = new Dictionary<string, OpenApiHeader> + Headers = new Dictionary<string, IOpenApiHeader> { { "Retry-After", new OpenApiHeader @@ -23,7 +23,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter Description = "A hint for when to retry the operation in full seconds.", Schema = new OpenApiSchema { - Type = "integer", + Type = JsonSchemaType.Integer, Format = "int32" } } @@ -36,7 +36,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter Description = "A short plain-text reason why the server is not available.", Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "text" } } diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs index 8f57572696..5b048be913 100644 --- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs +++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs @@ -5,7 +5,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Extensions; using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -66,17 +66,10 @@ public class SecurityRequirementsOperationFilter : IOperationFilter return; } - operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); - operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + operation.Responses?.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses?.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); - var scheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthenticationSchemes.CustomAuthentication - }, - }; + var scheme = new OpenApiSecuritySchemeReference(AuthenticationSchemes.CustomAuthentication, null, null); // Add DefaultAuthorization scope to any endpoint that has a policy with a requirement that is a subset of DefaultAuthorization. if (!requiredScopes.Contains(DefaultAuthPolicy.AsSpan(), StringComparison.Ordinal)) diff --git a/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs b/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs new file mode 100644 index 0000000000..e4eb5be2b9 --- /dev/null +++ b/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs @@ -0,0 +1,56 @@ +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// <summary> +/// Document filter that fixes security scheme references after document generation. +/// </summary> +/// <remarks> +/// In Microsoft.OpenApi v2, <see cref="OpenApiSecuritySchemeReference"/> requires a resolved +/// <c>Target</c> to serialize correctly. References created without a host document (as in +/// operation filters) serialize as empty objects. This filter re-creates all security scheme +/// references with the document context so they resolve properly during serialization. +/// </remarks> +internal class SecuritySchemeReferenceFixupFilter : IDocumentFilter +{ + /// <inheritdoc /> + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + swaggerDoc.RegisterComponents(); + + if (swaggerDoc.Paths is null) + { + return; + } + + foreach (var pathItem in swaggerDoc.Paths.Values) + { + if (pathItem.Operations is null) + { + continue; + } + + foreach (var operation in pathItem.Operations.Values) + { + if (operation.Security is null) + { + continue; + } + + for (int i = 0; i < operation.Security.Count; i++) + { + var oldReq = operation.Security[i]; + var newReq = new OpenApiSecurityRequirement(); + foreach (var kvp in oldReq) + { + var fixedRef = new OpenApiSecuritySchemeReference(kvp.Key.Reference.Id!, swaggerDoc); + newReq[fixedRef] = kvp.Value; + } + + operation.Security[i] = newReq; + } + } + } + } +} From 01eb56f04753381bdc8d4a9d2b16d0901bb69afc Mon Sep 17 00:00:00 2001 From: Andrew Rabert <ar@nullsum.net> Date: Thu, 19 Feb 2026 23:53:48 -0500 Subject: [PATCH 237/390] Mitigate pull_request_target privilege escalation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hotfix — replaces pull_request_target with pull_request to stop granting write permissions and secrets to fork PRs. Some workflows will break; can be fixed properly later. --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 6 +++--- .github/workflows/commands.yml | 2 +- .github/workflows/project-automation.yml | 2 +- .github/workflows/pull-request-conflict.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 23a82a1b2b..8e3717b332 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -1,6 +1,6 @@ name: ABI Compatibility on: - pull_request_target: + pull_request: permissions: {} @@ -77,7 +77,7 @@ jobs: pull-requests: write # to create or update comment (peter-evans/create-or-update-comment) name: ABI - Difference - if: ${{ github.event_name == 'pull_request_target' }} + if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest needs: - abi-head diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 08eedd54f7..3d04ac5e0b 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -5,7 +5,7 @@ on: - master tags: - 'v*' - pull_request_target: + pull_request: permissions: {} @@ -78,7 +78,7 @@ jobs: pull-requests: write name: OpenAPI - Difference - if: ${{ github.event_name == 'pull_request_target' }} + if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest needs: - openapi-head @@ -109,7 +109,7 @@ jobs: publish-unstable: name: OpenAPI - Publish Unstable Spec - if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} + if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} runs-on: ubuntu-latest needs: - openapi-head diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 2c4efcc8ca..2adb8f1010 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -4,7 +4,7 @@ on: types: - created - edited - pull_request_target: + pull_request: types: - labeled - synchronize diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index 7b29d3c817..9a9f3214a7 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -4,7 +4,7 @@ on: push: branches: - master - pull_request_target: + pull_request: issue_comment: permissions: {} diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index e6a9bf0caa..b003636a6e 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -4,7 +4,7 @@ on: push: branches: - master - pull_request_target: + pull_request: issue_comment: permissions: {} @@ -16,7 +16,7 @@ jobs: steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}} with: dirtyLabel: 'merge conflict' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' From 716f4c81982bbb31eab92b774633bf95cece44b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:41:35 +0000 Subject: [PATCH 238/390] Update github/codeql-action action to v4.32.4 --- .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 10fb775fea..66fa73d25b 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@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 From 56a469d8c3c0eed0d809d42a60d847e67ab61671 Mon Sep 17 00:00:00 2001 From: INOUE Daisuke <inoue.daisuke@gmail.com> Date: Fri, 20 Feb 2026 20:46:18 -0500 Subject: [PATCH 239/390] Translated using Weblate (Japanese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- .../Localization/Core/ja.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index d564d54cef..bdca8ae1cd 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -43,32 +43,32 @@ "NameInstallFailed": "{0}のインストールに失敗しました", "NameSeasonNumber": "シーズン {0}", "NameSeasonUnknown": "シーズン不明", - "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。", + "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロードできます。", "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります", "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です", "NotificationOptionAudioPlayback": "オーディオの再生を開始", - "NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました", + "NotificationOptionAudioPlaybackStopped": "オーディオの再生を停止", "NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました", "NotificationOptionInstallationFailed": "インストール失敗", "NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました", "NotificationOptionPluginError": "プラグインに障害が発生しました", - "NotificationOptionPluginInstalled": "プラグインがインストールされました", - "NotificationOptionPluginUninstalled": "プラグインがアンインストールされました", + "NotificationOptionPluginInstalled": "プラグインをインストールしました", + "NotificationOptionPluginUninstalled": "プラグインをアンインストールしました", "NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました", "NotificationOptionServerRestartRequired": "サーバーを再起動してください", "NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗", "NotificationOptionUserLockedOut": "ユーザーはロックされています", - "NotificationOptionVideoPlayback": "ビデオの再生を開始しました", - "NotificationOptionVideoPlaybackStopped": "ビデオを停止しました", + "NotificationOptionVideoPlayback": "ビデオの再生を開始", + "NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止", "Photos": "フォト", "Playlists": "プレイリスト", "Plugin": "プラグイン", - "PluginInstalledWithName": "{0} がインストールされました", - "PluginUninstalledWithName": "{0} がアンインストールされました", - "PluginUpdatedWithName": "{0} が更新されました", + "PluginInstalledWithName": "{0} をインストールしました", + "PluginUninstalledWithName": "{0} をアンインストールしました", + "PluginUpdatedWithName": "{0} を更新しました", "ProviderValue": "プロバイダ: {0}", "ScheduledTaskFailedWithName": "{0} が失敗しました", - "ScheduledTaskStartedWithName": "{0} が開始されました", + "ScheduledTaskStartedWithName": "{0} を開始", "ServerNameNeedsToBeRestarted": "{0} を再起動してください", "Shows": "番組", "Songs": "曲", From ccf2d15d5dbb3457a712668ba3e4cc54119fe736 Mon Sep 17 00:00:00 2001 From: Evan Champion <110177090+evan314159@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:59:57 +0800 Subject: [PATCH 240/390] AIFF support: add .aifc as audio file type, remove .aiff as image file type --- Emby.Naming/Common/NamingOptions.cs | 1 + src/Jellyfin.Drawing/ImageProcessor.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index f61ca7e129..9103174d2c 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -225,6 +225,7 @@ namespace Emby.Naming.Common ".afc", ".amf", ".aif", + ".aifc", ".aiff", ".alac", ".amr", diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index 46e5213a8c..6ffb022842 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -85,7 +85,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable "jpeg", "jpg", "png", - "aiff", "cr2", "crw", "nef", From 01b3c6f902cddde6f46727b5f766f3439d2aa223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:40:26 +0000 Subject: [PATCH 241/390] Update dependency Microsoft.NET.Test.Sdk to 18.3.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a520b87e2b..30e878c614 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Moq" Version="4.18.4" /> From e8232d31ab5367de015a85e4c52f18e1b883297d Mon Sep 17 00:00:00 2001 From: Pavel Miniutka <pavel.miniutka@gmail.com> Date: Thu, 26 Feb 2026 01:54:06 -0500 Subject: [PATCH 242/390] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index cb11cc0894..8ef3d9afdc 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -123,10 +123,10 @@ "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskRefreshChannels": "Абнавіць каналы", "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры", - "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.", + "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.", "TaskRefreshTrickplayImages": "Стварыць выявы Trickplay", "TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.", - "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты", + "TaskCleanCollectionsAndPlaylists": "Ачысціць калекцыі і плэй-лісты", "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.", "TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.", "TaskAudioNormalization": "Нармалізацыя гуку", From bdfb6edfa3bb0ed1b4876edb3c1cf8f42b7486de Mon Sep 17 00:00:00 2001 From: dfederm <david.federman@outlook.com> Date: Thu, 26 Feb 2026 13:54:31 -0500 Subject: [PATCH 243/390] Backport pull request #16150 from jellyfin/release-10.11.z Fix nullref in Season.GetEpisodes when the season is detached from a series Original-merge: b65daeca0baf7a447185205461b4a9d758bbeb26 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/TV/Season.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index b972ebaa6b..4360253b01 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -201,12 +201,17 @@ namespace MediaBrowser.Controller.Entities.TV public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes) { + if (series is null) + { + return []; + } + return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes); } public List<BaseItem> GetEpisodes() { - return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true); + return GetEpisodes(Series, null, null, new DtoOptions(true), true); } public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) From 2068be12219a9bc046b455dbce369a10f69dadf9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:50:41 +0000 Subject: [PATCH 244/390] Update GitHub Artifact Actions --- .github/workflows/ci-compat.yml | 8 ++++---- .github/workflows/ci-openapi.yml | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 8e3717b332..159492770d 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 3d04ac5e0b..e267836bdd 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -29,7 +29,7 @@ jobs: 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: openapi-head retention-days: 14 @@ -66,7 +66,7 @@ jobs: 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: openapi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: openapi-head path: openapi-head From bc316b3dc855e93d4d11e2c0d73d70326c38b889 Mon Sep 17 00:00:00 2001 From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:00:05 +0400 Subject: [PATCH 245/390] Fix near-1:1 SAR values falsely flagged as anamorphic Encoders sometimes produce sample aspect ratios like 3201:3200 (0.03% off square) for content that has effectively square pixels. The exact string comparison against "1:1" marks these as anamorphic, which triggers unnecessary transcoding on clients that require non-anamorphic video. Parse the SAR ratio numerically and treat values within 1% of 1:1 as square pixels. This threshold is well clear of the nearest real anamorphic SAR (PAL 4:3 at 16:15 = 6.67% off). --- .../Probing/ProbeResultNormalizer.cs | 28 ++++++++++++++++++- .../Probing/ProbeResultNormalizerTests.cs | 18 ++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index dbe5322897..471df369ba 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -863,7 +863,7 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.IsAnamorphic = false; } - else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) + else if (IsNearSquarePixelSar(streamInfo.SampleAspectRatio)) { stream.IsAnamorphic = false; } @@ -1154,6 +1154,32 @@ namespace MediaBrowser.MediaEncoding.Probing return Math.Abs(d1 - d2) <= variance; } + /// <summary> + /// Determines whether a sample aspect ratio represents square (or near-square) pixels. + /// Some encoders produce SARs like 3201:3200 for content that is effectively 1:1, + /// which would be falsely classified as anamorphic by an exact string comparison. + /// A 1% tolerance safely covers encoder rounding artifacts while preserving detection + /// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off). + /// </summary> + internal static bool IsNearSquarePixelSar(string sar) + { + if (string.IsNullOrEmpty(sar)) + { + return false; + } + + var parts = sar.Split(':'); + if (parts.Length == 2 + && double.TryParse(parts[0], CultureInfo.InvariantCulture, out var num) + && double.TryParse(parts[1], CultureInfo.InvariantCulture, out var den) + && den > 0) + { + return IsClose(num / den, 1.0, 0.01); + } + + return string.Equals(sar, "1:1", StringComparison.Ordinal); + } + /// <summary> /// Gets a frame rate from a string value in ffprobe output /// This could be a number or in the format of 2997/125. diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 8a2f84734e..40f853699b 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -39,6 +39,23 @@ namespace Jellyfin.MediaEncoding.Tests.Probing public void GetFrameRate_Success(string value, float? expected) => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); + [Theory] + [InlineData("1:1", true)] // exact square pixels + [InlineData("3201:3200", true)] // 0.03% off — encoder rounding artifact (4K HEVC) + [InlineData("1215:1216", true)] // 0.08% off — encoder rounding artifact + [InlineData("1001:1000", true)] // 0.1% off — encoder rounding artifact + [InlineData("16:15", false)] // 6.67% off — PAL DVD 4:3, genuinely anamorphic + [InlineData("8:9", false)] // 11.1% off — NTSC DVD 4:3 + [InlineData("32:27", false)] // 18.5% off — NTSC DVD 16:9 + [InlineData("10:11", false)] // 9.1% off — DV NTSC + [InlineData("64:45", false)] // 42.2% off — PAL DVD 16:9 + [InlineData("4:3", false)] // 33.3% off — classic anamorphic + [InlineData("0:1", false)] // invalid/unknown SAR + [InlineData("", false)] // empty + [InlineData(null, false)] // null + public void IsNearSquarePixelSar_DetectsCorrectly(string sar, bool expected) + => Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar)); + [Fact] public void GetMediaInfo_MetaData_Success() { @@ -123,6 +140,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(358, res.VideoStream.Height); Assert.Equal(720, res.VideoStream.Width); Assert.Equal("2.40:1", res.VideoStream.AspectRatio); + Assert.True(res.VideoStream.IsAnamorphic); // SAR 32:27 — genuinely anamorphic NTSC DVD 16:9 Assert.Equal("yuv420p", res.VideoStream.PixelFormat); Assert.Equal(31d, res.VideoStream.Level); Assert.Equal(1, res.VideoStream.RefFrames); From d55f082579933cad112a6fed6caa6130bf0d838c Mon Sep 17 00:00:00 2001 From: JPVenson <github@jpb.email> Date: Sun, 1 Mar 2026 11:50:37 +0100 Subject: [PATCH 246/390] Merge pull request #16281 from jellyfin/JPVenson-patch-1 Revise note on hosting web client for development --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e546e7f115..46f531ff45 100644 --- a/README.md +++ b/README.md @@ -94,13 +94,12 @@ git clone https://github.com/jellyfin/jellyfin.git The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly. -Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. +Note that it is recommended for development to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. -There are three options to get the files for the web client. +There are two options to get the files for the web client. -1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page. -2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) -3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` +1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) +2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` ### Running The Server From d2f733f9a4d91a09ab887d410b2778a916f27c7d Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Sun, 1 Mar 2026 05:57:22 -0500 Subject: [PATCH 247/390] Backport pull request #16204 from jellyfin/release-10.11.z Fix broken library subtitle download settings Original-merge: ca57166e95858f9c767b6279df6e5abfe9212700 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../FixLibrarySubtitleDownloadLanguages.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs new file mode 100644 index 0000000000..e82123e5ac --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +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; + +/// <summary> +/// Migration to fix broken library subtitle download languages. +/// </summary> +[JellyfinMigration("2026-02-06T20:00:00", nameof(FixLibrarySubtitleDownloadLanguages))] +internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine +{ + private readonly ILocalizationManager _localizationManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="FixLibrarySubtitleDownloadLanguages"/> class. + /// </summary> + /// <param name="localizationManager">The Localization manager.</param> + /// <param name="startupLogger">The startup logger for Startup UI integration.</param> + /// <param name="libraryManager">The Library manager.</param> + /// <param name="logger">The logger.</param> + public FixLibrarySubtitleDownloadLanguages( + ILocalizationManager localizationManager, + IStartupLogger<FixLibrarySubtitleDownloadLanguages> startupLogger, + ILibraryManager libraryManager, + ILogger<FixLibrarySubtitleDownloadLanguages> logger) + { + _localizationManager = localizationManager; + _libraryManager = libraryManager; + _logger = startupLogger.With(logger); + } + + /// <inheritdoc /> + public Task PerformAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting to fix library subtitle download languages."); + + var virtualFolders = _libraryManager.GetVirtualFolders(false); + + foreach (var virtualFolder in virtualFolders) + { + var options = virtualFolder.LibraryOptions; + if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) + { + continue; + } + + // Some virtual folders don't have a proper item id. + if (!Guid.TryParse(virtualFolder.ItemId, out var folderId)) + { + continue; + } + + var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId); + if (collectionFolder is null) + { + _logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId); + continue; + } + + var fixedLanguages = new List<string>(); + + foreach (var language in options.SubtitleDownloadLanguages) + { + var foundLanguage = _localizationManager.FindLanguageInfo(language)?.ThreeLetterISOLanguageName; + if (foundLanguage is not null) + { + // Converted ISO 639-2/B to T (ger to deu) + if (!string.Equals(foundLanguage, language, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Converted '{Language}' to '{ResolvedLanguage}' in library '{LibraryName}'.", language, foundLanguage, virtualFolder.Name); + } + + if (fixedLanguages.Contains(foundLanguage, StringComparer.OrdinalIgnoreCase)) + { + _logger.LogInformation("Language '{Language}' already exists for library '{LibraryName}'. Skipping duplicate.", foundLanguage, virtualFolder.Name); + continue; + } + + fixedLanguages.Add(foundLanguage); + } + else + { + _logger.LogInformation("Could not resolve language '{Language}' in library '{LibraryName}'. Skipping.", language, virtualFolder.Name); + } + } + + options.SubtitleDownloadLanguages = [.. fixedLanguages]; + collectionFolder.UpdateLibraryOptions(options); + } + + _logger.LogInformation("Library subtitle download languages fixed."); + + return Task.CompletedTask; + } +} From f680495ca377b20488cc8133a054317d6daf48fc Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 1 Mar 2026 05:57:23 -0500 Subject: [PATCH 248/390] Backport pull request #16253 from jellyfin/release-10.11.z Checkpoint WAL before moving library.db in migration Original-merge: b6a96513de5fa301db83c6adab47fe64db0ff48e Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Migrations/Routines/MigrateLibraryDb.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 70761fa7db..c6ac55b6eb 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -464,6 +464,16 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine SqliteConnection.ClearAllPools(); + using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}")) + { + checkpointConnection.Open(); + using var cmd = checkpointConnection.CreateCommand(); + cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + cmd.ExecuteNonQuery(); + } + + SqliteConnection.ClearAllPools(); + _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); File.Move(libraryDbPath, libraryDbPath + ".old", true); } From d87fe973f3516e20ae4c4ecd8036286deeb4b51d Mon Sep 17 00:00:00 2001 From: NoFear0411 <9083405+NoFear0411@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:51:27 +0400 Subject: [PATCH 249/390] Fix StyleCop and xUnit analyzer errors - Add missing param and returns XML doc tags (SA1611, SA1615) - Remove trailing alignment whitespace in test attributes (SA1025) - Use nullable string parameter for null test case (xUnit1012) --- .../Probing/ProbeResultNormalizer.cs | 2 ++ .../Probing/ProbeResultNormalizerTests.cs | 28 +++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 471df369ba..127bdd380d 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1161,6 +1161,8 @@ namespace MediaBrowser.MediaEncoding.Probing /// A 1% tolerance safely covers encoder rounding artifacts while preserving detection /// of genuine anamorphic content (closest standard is PAL 4:3 at 16:15 = 6.67% off). /// </summary> + /// <param name="sar">The sample aspect ratio string in "N:D" format.</param> + /// <returns><c>true</c> if the SAR is within 1% of 1:1; otherwise <c>false</c>.</returns> internal static bool IsNearSquarePixelSar(string sar) { if (string.IsNullOrEmpty(sar)) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 40f853699b..8ebbd029ac 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -40,20 +40,20 @@ namespace Jellyfin.MediaEncoding.Tests.Probing => Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value)); [Theory] - [InlineData("1:1", true)] // exact square pixels - [InlineData("3201:3200", true)] // 0.03% off — encoder rounding artifact (4K HEVC) - [InlineData("1215:1216", true)] // 0.08% off — encoder rounding artifact - [InlineData("1001:1000", true)] // 0.1% off — encoder rounding artifact - [InlineData("16:15", false)] // 6.67% off — PAL DVD 4:3, genuinely anamorphic - [InlineData("8:9", false)] // 11.1% off — NTSC DVD 4:3 - [InlineData("32:27", false)] // 18.5% off — NTSC DVD 16:9 - [InlineData("10:11", false)] // 9.1% off — DV NTSC - [InlineData("64:45", false)] // 42.2% off — PAL DVD 16:9 - [InlineData("4:3", false)] // 33.3% off — classic anamorphic - [InlineData("0:1", false)] // invalid/unknown SAR - [InlineData("", false)] // empty - [InlineData(null, false)] // null - public void IsNearSquarePixelSar_DetectsCorrectly(string sar, bool expected) + [InlineData("1:1", true)] + [InlineData("3201:3200", true)] + [InlineData("1215:1216", true)] + [InlineData("1001:1000", true)] + [InlineData("16:15", false)] + [InlineData("8:9", false)] + [InlineData("32:27", false)] + [InlineData("10:11", false)] + [InlineData("64:45", false)] + [InlineData("4:3", false)] + [InlineData("0:1", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void IsNearSquarePixelSar_DetectsCorrectly(string? sar, bool expected) => Assert.Equal(expected, ProbeResultNormalizer.IsNearSquarePixelSar(sar)); [Fact] From 8715cb5b9ea7f2350c03890277ae651711717e8d Mon Sep 17 00:00:00 2001 From: Anthony Lavado <anthony@lavado.ca> Date: Sun, 1 Mar 2026 12:38:07 -0500 Subject: [PATCH 250/390] Update JetBrains logo link in README.md Update the logo to match the current branding that has been live for a while now. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 46f531ff45..7531481860 100644 --- a/README.md +++ b/README.md @@ -197,5 +197,5 @@ This project is supported by: <br/> <a href="https://www.digitalocean.com"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" height="50px" alt="DigitalOcean"></a>   -<a href="https://www.jetbrains.com"><img src="https://gist.githubusercontent.com/anthonylavado/e8b2403deee9581e0b4cb8cd675af7db/raw/fa104b7d73f759d7262794b94569f1b89df41c0b/jetbrains.svg" height="50px" alt="JetBrains logo"></a> +<a href="https://www.jetbrains.com"><img src="https://gist.githubusercontent.com/anthonylavado/e8b2403deee9581e0b4cb8cd675af7db/raw/199ae22980ef5da64882ec2de3e8e5c03fe535b8/jetbrains.svg" height="50px" alt="JetBrains logo"></a> </p> 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 251/390] 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 <chiburiki12@gmail.com> Date: Tue, 3 Mar 2026 11:09:13 -0500 Subject: [PATCH 252/390] 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 <juancalderonmicro@hotmail.com> Date: Mon, 2 Mar 2026 13:15:48 -0500 Subject: [PATCH 253/390] 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 <noreply@anthropic.com> --- 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 /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> [HttpPost("Initiate")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task<ActionResult<QuickConnectResult>> 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 254/390] 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 <chiburiki12@gmail.com> Date: Tue, 3 Mar 2026 21:03:13 -0500 Subject: [PATCH 255/390] 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 256/390] 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 @@ <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" /> <PackageVersion Include="prometheus-net" Version="8.2.1" /> - <PackageVersion Include="Polly" Version="8.6.5" /> + <PackageVersion Include="Polly" Version="8.6.6" /> <PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> From 8824f07e1b07e5dbcd0641423dc472b67c268d21 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 4 Mar 2026 20:14:21 +0100 Subject: [PATCH 257/390] 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 <eugclass@gmail.com> Date: Wed, 4 Mar 2026 15:19:55 -0500 Subject: [PATCH 258/390] 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 259/390] 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 260/390] 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 261/390] 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 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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 <Ghost_of_Stone@web.de> Date: Sat, 7 Mar 2026 10:49:42 +0100 Subject: [PATCH 262/390] 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 <redinsch@gmx.de> Date: Sun, 8 Mar 2026 11:29:54 +0100 Subject: [PATCH 263/390] 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 { /// <summary> - /// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, prioritizing "en" over other non-matches. + /// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, then "en", then no language, over other non-matches. /// </summary> /// <param name="remoteImageInfos">The remote image infos.</param> /// <param name="requestedLanguage">The requested language for the images.</param> @@ -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 <redinsch@gmx.de> Date: Sun, 8 Mar 2026 12:02:08 +0100 Subject: [PATCH 264/390] 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 <denislavkdenev@gmail.com> Date: Mon, 9 Mar 2026 20:32:08 -0400 Subject: [PATCH 265/390] 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 <theguymadmax@proton.me> Date: Tue, 10 Mar 2026 19:04:02 -0400 Subject: [PATCH 266/390] 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<string> 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 267/390] 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 @@ <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> - <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /> + <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" /> From e4eba084dd0e852e90c7677f9a8b594ecd3f7669 Mon Sep 17 00:00:00 2001 From: jaxx2104 <jaxx2104@gmail.com> Date: Wed, 11 Mar 2026 23:57:30 +0900 Subject: [PATCH 268/390] Use generic Enum overloads to resolve CA2263 warnings Replace Enum.Parse(typeof(T), ...) and Enum.GetNames(typeof(T)) with their generic counterparts Enum.Parse<T>() and Enum.GetNames<T>() in MediaBrowser.Model/Dlna for improved type safety. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- 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<TransportStreamTimestamp>(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<VideoRangeType>().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 269/390] 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 270/390] 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 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.3" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.4" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> - <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" /> - <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /> + <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" /> + <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.4" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> @@ -77,7 +77,7 @@ <PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.4" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" /> - <PackageVersion Include="System.Text.Json" Version="10.0.3" /> + <PackageVersion Include="System.Text.Json" Version="10.0.4" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> 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 271/390] 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 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.4.1" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.4" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.5" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" /> <PackageVersion Include="System.Text.Json" Version="10.0.4" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> From 946c6b9981145d73a6cd64fc6fbcbd6d5b6961ae Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Wed, 11 Mar 2026 21:20:14 +0100 Subject: [PATCH 272/390] 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<Folder>(); + 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<ItemFilter[]> ApplyFilters_Invalid() + { + var data = new TheoryData<ItemFilter[]>(); + data.Add([ItemFilter.IsFolder, ItemFilter.IsNotFolder]); + data.Add([ItemFilter.IsPlayed, ItemFilter.IsUnplayed]); + data.Add([ItemFilter.Likes, ItemFilter.Dislikes]); + return data; + } + + [Theory] + [MemberData(nameof(ApplyFilters_Invalid))] + public void ApplyFilters_Invalid_ThrowsArgumentException(ItemFilter[] filters) + { + var query = new InternalItemsQuery(); + Assert.Throws<ArgumentException>(() => query.ApplyFilters(filters)); + } +} 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 273/390] 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 274/390] 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 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.4" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.4" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.5" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.4" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.4" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.4" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> @@ -77,7 +77,7 @@ <PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.5" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" /> - <PackageVersion Include="System.Text.Json" Version="10.0.4" /> + <PackageVersion Include="System.Text.Json" Version="10.0.5" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> From 37983c943a7f65850cc2bd4854f22dbe2ae92c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Nie=C5=82acny?= <piotr.nielacny@gmail.com> Date: Fri, 13 Mar 2026 16:34:44 +0100 Subject: [PATCH 275/390] Respect EnableSubtitleExtraction setting in subtitle delivery Wire up EnableSubtitleExtraction config to MediaEncoder.CanExtractSubtitles so the setting is actually respected. Gate subtitle extraction check behind PlayMethod.Transcode since DirectPlay has no competing ffmpeg process. Add parameterized tests for StreamBuilder.GetSubtitleProfile covering text and graphical codecs, profile format matching, and extraction setting behavior. Remove misplaced SubtitleEncoder extraction test. --- .../Encoder/MediaEncoder.cs | 3 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- .../Dlna/StreamBuilderTests.cs | 55 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 73c5b88c8b..770965cab3 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1331,8 +1331,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public bool CanExtractSubtitles(string codec) { - // TODO is there ever a case when a subtitle can't be extracted?? - return true; + return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction; } private sealed class ProcessWrapper : IDisposable diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 42cb208d08..166d574d25 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1555,7 +1555,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) + if (!subtitleStream.IsExternal && playMethod == PlayMethod.Transcode && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) { continue; } diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 2c1080ffe3..8269ae58cd 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests return (path, query, filename, extension); } + + [Theory] + // EnableSubtitleExtraction = false, internal subtitles + [InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + [InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + [InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + // EnableSubtitleExtraction = false, external subtitles + [InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + // EnableSubtitleExtraction = true, internal subtitles + [InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + // EnableSubtitleExtraction = true, external subtitles + [InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + public void GetSubtitleProfile_RespectsExtractionSetting( + string codec, + string profileFormat, + bool enableSubtitleExtraction, + bool isExternal, + PlayMethod playMethod, + SubtitleDeliveryMethod expectedMethod) + { + var mediaSource = new MediaSourceInfo(); + var subtitleStream = new MediaStream + { + Type = MediaStreamType.Subtitle, + Index = 0, + IsExternal = isExternal, + Path = isExternal ? "/media/sub." + codec : null, + Codec = codec, + SupportsExternalStream = MediaStream.IsTextFormat(codec) + }; + + var subtitleProfiles = new[] + { + new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External } + }; + + var transcoderSupport = new Mock<ITranscoderSupport>(); + transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny<string>())).Returns(enableSubtitleExtraction); + + var result = StreamBuilder.GetSubtitleProfile( + mediaSource, + subtitleStream, + subtitleProfiles, + playMethod, + transcoderSupport.Object, + null, + null); + + Assert.Equal(expectedMethod, result.Method); + } } } From 3997e016fa092f8fad171ce5f24f619b552e5f78 Mon Sep 17 00:00:00 2001 From: lowbit <spaha92@gmail.com> Date: Fri, 13 Mar 2026 15:33:06 -0400 Subject: [PATCH 276/390] Backport pull request #16257 from jellyfin/release-10.11.z Fix subtitle extraction caching empty files Original-merge: 6864e108b8b36ad25655e683b2cf2abf8b8ca346 Merged-by: joshuaboniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com> --- .../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 <IceStormNG@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:33:07 -0400 Subject: [PATCH 277/390] 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 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../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 <nst799610810@gmail.com> Date: Fri, 13 Mar 2026 15:33:08 -0400 Subject: [PATCH 278/390] Backport pull request #16392 from jellyfin/release-10.11.z Fix filter detection in FFmpeg 8.1 Original-merge: 55c00d76bbbe2d2759f33fab673f26b26093a30e Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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(?<codec>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] private static partial Regex CodecRegex(); - [GeneratedRegex("^\\s\\S{3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] + [GeneratedRegex("^\\s\\S{2,3}\\s(?<filter>[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] private static partial Regex FilterRegex(); } } From d65960fe5d3f798ea3ac0527abb96b3327a39ba4 Mon Sep 17 00:00:00 2001 From: Benjamin Lea <benjaminshalomlea@gmail.com> Date: Fri, 13 Mar 2026 17:30:07 -0400 Subject: [PATCH 279/390] 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 <Ghost_of_Stone@web.de> Date: Sat, 14 Mar 2026 19:58:43 +0100 Subject: [PATCH 280/390] 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); + } + } } /// <inheritdoc /> 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 /// <param name="item">The base item to extract segments from.</param> /// <returns>True if item is supported, otherwise false.</returns> ValueTask<bool> Supports(BaseItem item); + + /// <summary> + /// Called when extracted segment data for an item is being pruned. + /// Providers should delete any cached analysis data they hold for the given item. + /// </summary> + /// <param name="itemId">The item whose data is being pruned.</param> + /// <param name="cancellationToken">Abort token.</param> + /// <returns>A task representing the asynchronous cleanup operation.</returns> + Task CleanupExtractedData(Guid itemId, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } From c0c474021519d4485a3f5724fcae1be8ffc45ba8 Mon Sep 17 00:00:00 2001 From: Lofuuzi <lather0519@gmail.com> Date: Sat, 14 Mar 2026 14:37:46 -0400 Subject: [PATCH 281/390] 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 <lather0519@gmail.com> Date: Sun, 15 Mar 2026 02:22:50 -0400 Subject: [PATCH 282/390] 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 <lather0519@gmail.com> Date: Sun, 15 Mar 2026 03:24:21 -0400 Subject: [PATCH 283/390] 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 284/390] 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 <lather0519@gmail.com> Date: Mon, 16 Mar 2026 13:55:08 -0400 Subject: [PATCH 285/390] 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 <lather0519@gmail.com> Date: Tue, 17 Mar 2026 09:17:28 -0400 Subject: [PATCH 286/390] 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 287/390] 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 @@ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> - <PackageVersion Include="coverlet.collector" Version="8.0.0" /> + <PackageVersion Include="coverlet.collector" Version="8.0.1" /> <PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> From dc4d3639e00c7f66e9eefffc3cf2bf1ad9e37669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84rik?= <kwakz4life@gmail.com> Date: Tue, 17 Mar 2026 16:26:49 -0400 Subject: [PATCH 288/390] 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 <lather0519@gmail.com> Date: Thu, 19 Mar 2026 04:49:05 -0400 Subject: [PATCH 289/390] 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 <lousando@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:31:29 -0500 Subject: [PATCH 290/390] 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 <louis@sandoval.family> --- .../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<Book> { - 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 <lather0519@gmail.com> Date: Fri, 20 Mar 2026 15:31:36 -0400 Subject: [PATCH 291/390] 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 <lednurb@live.nl> Date: Sun, 22 Mar 2026 02:26:44 -0400 Subject: [PATCH 292/390] 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 <nst799610810@gmail.com> Date: Mon, 23 Mar 2026 17:06:22 -0400 Subject: [PATCH 293/390] Backport pull request #16423 from jellyfin/release-10.11.z Fix readrate options in FFmpeg 8.1 Original-merge: 29b236185701091f6719862b05bd7bda58d88475 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../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<string>(); 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 294/390] 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 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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" <joshua@boniface.me> Date: Mon, 23 Mar 2026 17:08:15 -0400 Subject: [PATCH 295/390] 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 } /// <summary> - /// Gets the free space of a specific directory. + /// Gets the free space of the parent filesystem of a specific directory. /// </summary> /// <param name="path">Path to a folder.</param> - /// <returns>The number of bytes available space.</returns> + /// <returns>Various details about the parent filesystem containing the directory.</returns> 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 } } + /// <summary> + /// Walk a path and fully resolve any symlinks within it. + /// </summary> + 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; + } + /// <summary> /// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold. /// </summary> From 434ebc8b110a2736c9be08360c17cf74e27803d1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Mon, 23 Mar 2026 17:11:29 -0400 Subject: [PATCH 296/390] 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" <joshua@boniface.me> Date: Mon, 23 Mar 2026 17:15:49 -0400 Subject: [PATCH 297/390] 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; } /// <summary> - /// Gets the free space of the underlying storage device of the <see cref="Path"/>. + /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present) + /// </summary> + public required string ResolvedPath { get; init; } + + /// <summary> + /// Gets the free space of the underlying storage device of the <see cref="ResolvedPath"/>. /// </summary> public long FreeSpace { get; init; } /// <summary> - /// Gets the used space of the underlying storage device of the <see cref="Path"/>. + /// Gets the used space of the underlying storage device of the <see cref="ResolvedPath"/>. /// </summary> public long UsedSpace { get; init; } /// <summary> - /// Gets the kind of storage device of the <see cref="Path"/>. + /// Gets the kind of storage device of the <see cref="ResolvedPath"/>. /// </summary> public string? StorageType { get; init; } From 8142bbd50e4c2218e99c621900430b0189c267c3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Mon, 23 Mar 2026 17:22:35 -0400 Subject: [PATCH 298/390] 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" <joshua@boniface.me> Date: Mon, 23 Mar 2026 23:09:56 -0400 Subject: [PATCH 299/390] Apply suggestions from code review Co-authored-by: JPVenson <ger-delta-07@hotmail.de> --- .../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 <christian.scheil@icloud.com> Date: Tue, 24 Mar 2026 18:02:00 +0100 Subject: [PATCH 300/390] 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 301/390] 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 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.4.1" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.5" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.6" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.6" /> <PackageVersion Include="System.Text.Json" Version="10.0.5" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> From c22933260b1d9b8cd97980c00a70f53bbaaf4f54 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Tue, 24 Mar 2026 22:22:52 -0400 Subject: [PATCH 302/390] 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; } /// <summary> - /// 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). /// </summary> public required string ResolvedPath { get; init; } From fec78c8448bd19f96460d853732cf24812443b70 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Tue, 24 Mar 2026 22:31:17 -0400 Subject: [PATCH 303/390] 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 <dkanada@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:39:31 +0900 Subject: [PATCH 304/390] 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 305/390] 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 @@ <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="Svg.Skia" Version="3.4.1" /> - <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.6" /> - <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.6" /> + <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" /> + <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageVersion Include="System.Text.Json" Version="10.0.5" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.11.0" /> From 9c09e7113e9eaadcf691e0fae68256a940a8b989 Mon Sep 17 00:00:00 2001 From: Lofuuzi <lather0519@gmail.com> Date: Wed, 25 Mar 2026 19:40:34 -0400 Subject: [PATCH 306/390] 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 307/390] 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 308/390] 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 <theguymadmax@proton.me> Date: Fri, 27 Mar 2026 13:48:30 -0400 Subject: [PATCH 309/390] 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<BaseItem> 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 <pjcfifa@gmail.com> Date: Sat, 28 Mar 2026 16:25:01 -0400 Subject: [PATCH 310/390] reverse check for track changed Signed-off-by: Patrick Cunniff <pjcfifa@gmail.com> --- 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 311/390] 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; } + /// <summary> + /// 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. + /// </summary> + /// <param name="builder">StringBuilder for the master playlist.</param> + /// <param name="state">StreamState of the current stream.</param> + /// <param name="url">Playlist URL for this variant.</param> + /// <param name="bitrate">Bitrate for the BANDWIDTH field.</param> + /// <param name="subtitleGroup">Subtitle group identifier, or null.</param> + 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); + } + /// <summary> /// Appends a VIDEO-RANGE field containing the range of the output video stream. /// </summary> 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(); } + + /// <summary> + /// Gets a Dolby Vision codec string for profiles without a compatible base layer. + /// </summary> + /// <param name="dvProfile">Dolby Vision profile number.</param> + /// <param name="dvLevel">Dolby Vision level number.</param> + /// <param name="codec">Video codec name (e.g. "hevc", "av1") to determine the DoVi FourCC.</param> + /// <returns>Dolby Vision codec string.</returns> + 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 <Ghost_of_Stone@web.de> Date: Sun, 29 Mar 2026 12:38:01 +0200 Subject: [PATCH 312/390] 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 <Ghost_of_Stone@web.de> Date: Sun, 29 Mar 2026 12:38:32 +0200 Subject: [PATCH 313/390] 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 314/390] 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 <christian.scheil@icloud.com> Date: Sun, 29 Mar 2026 12:39:16 +0200 Subject: [PATCH 315/390] 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 <bond.009@outlook.com> --- .../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 316/390] 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 <aimarshall615@gmail.com> --- 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<VideoFileInfo>().ToList(), _namingOptions).ToList(); - Assert.Equal(7, result.Count); - Assert.Empty(result[0].AlternateVersions); + Assert.Single(result); + Assert.Equal(6, result[0].AlternateVersions.Count); + + // Verify 3D recognition is preserved on alternate versions + var hsbs = result[0].AlternateVersions.First(v => v.Path.Contains("3d-hsbs", StringComparison.Ordinal)); + Assert.True(hsbs.Is3D); + Assert.Equal("hsbs", hsbs.Format3D); } [Fact] @@ -435,5 +441,39 @@ namespace Jellyfin.Naming.Tests.Video Assert.Empty(result); } + + [Fact] + public void Resolve_GivenUnderscoreSeparator_GroupsVersions() + { + var files = new[] + { + "/movies/Movie (2020)/Movie (2020)_4K.mkv", + "/movies/Movie (2020)/Movie (2020)_1080p.mkv" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Single(result[0].AlternateVersions); + } + + [Fact] + public void Resolve_GivenDotSeparator_GroupsVersions() + { + var files = new[] + { + "/movies/Movie (2020)/Movie (2020).UHD.mkv", + "/movies/Movie (2020)/Movie (2020).1080p.mkv" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Single(result[0].AlternateVersions); + } } } From a6da575785e678e64ed03978d1f4f60a80423121 Mon Sep 17 00:00:00 2001 From: Bond_009 <bond.009@outlook.com> Date: Sun, 29 Mar 2026 14:16:26 +0200 Subject: [PATCH 317/390] 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 /// </summary> /// <param name="dict">The dict.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict) + private static Dictionary<string, string?> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string?> dict) { - return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase); + return new Dictionary<string, string?>(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 /// </summary> /// <value>The profile.</value> [JsonPropertyName("profile")] - public string Profile { get; set; } + public string? Profile { get; set; } /// <summary> /// Gets or sets the codec_name. /// </summary> /// <value>The codec_name.</value> [JsonPropertyName("codec_name")] - public string CodecName { get; set; } + public string? CodecName { get; set; } /// <summary> /// Gets or sets the codec_long_name. /// </summary> /// <value>The codec_long_name.</value> [JsonPropertyName("codec_long_name")] - public string CodecLongName { get; set; } + public string? CodecLongName { get; set; } /// <summary> /// Gets or sets the codec_type. @@ -50,49 +48,49 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The sample_rate.</value> [JsonPropertyName("sample_rate")] - public string SampleRate { get; set; } + public string? SampleRate { get; set; } /// <summary> /// Gets or sets the channels. /// </summary> /// <value>The channels.</value> [JsonPropertyName("channels")] - public int Channels { get; set; } + public int? Channels { get; set; } /// <summary> /// Gets or sets the channel_layout. /// </summary> /// <value>The channel_layout.</value> [JsonPropertyName("channel_layout")] - public string ChannelLayout { get; set; } + public string? ChannelLayout { get; set; } /// <summary> /// Gets or sets the avg_frame_rate. /// </summary> /// <value>The avg_frame_rate.</value> [JsonPropertyName("avg_frame_rate")] - public string AverageFrameRate { get; set; } + public string? AverageFrameRate { get; set; } /// <summary> /// Gets or sets the duration. /// </summary> /// <value>The duration.</value> [JsonPropertyName("duration")] - public string Duration { get; set; } + public string? Duration { get; set; } /// <summary> /// Gets or sets the bit_rate. /// </summary> /// <value>The bit_rate.</value> [JsonPropertyName("bit_rate")] - public string BitRate { get; set; } + public string? BitRate { get; set; } /// <summary> /// Gets or sets the width. /// </summary> /// <value>The width.</value> [JsonPropertyName("width")] - public int Width { get; set; } + public int? Width { get; set; } /// <summary> /// Gets or sets the refs. @@ -106,21 +104,21 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The height.</value> [JsonPropertyName("height")] - public int Height { get; set; } + public int? Height { get; set; } /// <summary> /// Gets or sets the display_aspect_ratio. /// </summary> /// <value>The display_aspect_ratio.</value> [JsonPropertyName("display_aspect_ratio")] - public string DisplayAspectRatio { get; set; } + public string? DisplayAspectRatio { get; set; } /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> [JsonPropertyName("tags")] - public IReadOnlyDictionary<string, string> Tags { get; set; } + public IReadOnlyDictionary<string, string?>? Tags { get; set; } /// <summary> /// Gets or sets the bits_per_sample. @@ -141,7 +139,7 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The r_frame_rate.</value> [JsonPropertyName("r_frame_rate")] - public string RFrameRate { get; set; } + public string? RFrameRate { get; set; } /// <summary> /// Gets or sets the has_b_frames. @@ -155,70 +153,70 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The sample_aspect_ratio.</value> [JsonPropertyName("sample_aspect_ratio")] - public string SampleAspectRatio { get; set; } + public string? SampleAspectRatio { get; set; } /// <summary> /// Gets or sets the pix_fmt. /// </summary> /// <value>The pix_fmt.</value> [JsonPropertyName("pix_fmt")] - public string PixelFormat { get; set; } + public string? PixelFormat { get; set; } /// <summary> /// Gets or sets the level. /// </summary> /// <value>The level.</value> [JsonPropertyName("level")] - public int Level { get; set; } + public int? Level { get; set; } /// <summary> /// Gets or sets the time_base. /// </summary> /// <value>The time_base.</value> [JsonPropertyName("time_base")] - public string TimeBase { get; set; } + public string? TimeBase { get; set; } /// <summary> /// Gets or sets the start_time. /// </summary> /// <value>The start_time.</value> [JsonPropertyName("start_time")] - public string StartTime { get; set; } + public string? StartTime { get; set; } /// <summary> /// Gets or sets the codec_time_base. /// </summary> /// <value>The codec_time_base.</value> [JsonPropertyName("codec_time_base")] - public string CodecTimeBase { get; set; } + public string? CodecTimeBase { get; set; } /// <summary> /// Gets or sets the codec_tag. /// </summary> /// <value>The codec_tag.</value> [JsonPropertyName("codec_tag")] - public string CodecTag { get; set; } + public string? CodecTag { get; set; } /// <summary> - /// Gets or sets the codec_tag_string. + /// Gets or sets the codec_tag_string?. /// </summary> - /// <value>The codec_tag_string.</value> - [JsonPropertyName("codec_tag_string")] - public string CodecTagString { get; set; } + /// <value>The codec_tag_string?.</value> + [JsonPropertyName("codec_tag_string?")] + public string? CodecTagString { get; set; } /// <summary> /// Gets or sets the sample_fmt. /// </summary> /// <value>The sample_fmt.</value> [JsonPropertyName("sample_fmt")] - public string SampleFmt { get; set; } + public string? SampleFmt { get; set; } /// <summary> /// Gets or sets the dmix_mode. /// </summary> /// <value>The dmix_mode.</value> [JsonPropertyName("dmix_mode")] - public string DmixMode { get; set; } + public string? DmixMode { get; set; } /// <summary> /// Gets or sets the start_pts. @@ -232,90 +230,90 @@ namespace MediaBrowser.MediaEncoding.Probing /// </summary> /// <value>The is_avc.</value> [JsonPropertyName("is_avc")] - public bool IsAvc { get; set; } + public bool? IsAvc { get; set; } /// <summary> /// Gets or sets the nal_length_size. /// </summary> /// <value>The nal_length_size.</value> [JsonPropertyName("nal_length_size")] - public string NalLengthSize { get; set; } + public string? NalLengthSize { get; set; } /// <summary> /// Gets or sets the ltrt_cmixlev. /// </summary> /// <value>The ltrt_cmixlev.</value> [JsonPropertyName("ltrt_cmixlev")] - public string LtrtCmixlev { get; set; } + public string? LtrtCmixlev { get; set; } /// <summary> /// Gets or sets the ltrt_surmixlev. /// </summary> /// <value>The ltrt_surmixlev.</value> [JsonPropertyName("ltrt_surmixlev")] - public string LtrtSurmixlev { get; set; } + public string? LtrtSurmixlev { get; set; } /// <summary> /// Gets or sets the loro_cmixlev. /// </summary> /// <value>The loro_cmixlev.</value> [JsonPropertyName("loro_cmixlev")] - public string LoroCmixlev { get; set; } + public string? LoroCmixlev { get; set; } /// <summary> /// Gets or sets the loro_surmixlev. /// </summary> /// <value>The loro_surmixlev.</value> [JsonPropertyName("loro_surmixlev")] - public string LoroSurmixlev { get; set; } + public string? LoroSurmixlev { get; set; } /// <summary> /// Gets or sets the field_order. /// </summary> /// <value>The field_order.</value> [JsonPropertyName("field_order")] - public string FieldOrder { get; set; } + public string? FieldOrder { get; set; } /// <summary> /// Gets or sets the disposition. /// </summary> /// <value>The disposition.</value> [JsonPropertyName("disposition")] - public IReadOnlyDictionary<string, int> Disposition { get; set; } + public IReadOnlyDictionary<string, int>? Disposition { get; set; } /// <summary> /// Gets or sets the color range. /// </summary> /// <value>The color range.</value> [JsonPropertyName("color_range")] - public string ColorRange { get; set; } + public string? ColorRange { get; set; } /// <summary> /// Gets or sets the color space. /// </summary> /// <value>The color space.</value> [JsonPropertyName("color_space")] - public string ColorSpace { get; set; } + public string? ColorSpace { get; set; } /// <summary> /// Gets or sets the color transfer. /// </summary> /// <value>The color transfer.</value> [JsonPropertyName("color_transfer")] - public string ColorTransfer { get; set; } + public string? ColorTransfer { get; set; } /// <summary> /// Gets or sets the color primaries. /// </summary> /// <value>The color primaries.</value> [JsonPropertyName("color_primaries")] - public string ColorPrimaries { get; set; } + public string? ColorPrimaries { get; set; } /// <summary> /// Gets or sets the side_data_list. /// </summary> /// <value>The side_data_list.</value> [JsonPropertyName("side_data_list")] - public IReadOnlyList<MediaStreamInfoSideData> SideDataList { get; set; } + public IReadOnlyList<MediaStreamInfoSideData>? 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 /// <returns>MediaStream.</returns> private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList<MediaFrameInfo> 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 <bond.009@outlook.com> Date: Sun, 29 Mar 2026 14:28:41 -0400 Subject: [PATCH 318/390] Backport pull request #16369 from jellyfin/release-10.11.z Fix nullref ex in font handling Original-merge: 41c2d51d8cb9b4f9bdf81be6e73f7ae2d447a8d7 Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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<SkiaEncoder> _logger; private readonly IApplicationPaths _appPaths; private static readonly SKImageFilter _imageFilter; - private static readonly SKTypeface[] _typefaces; + private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); /// <summary> /// The default sampling options, equivalent to old high quality filter settings when upscaling. @@ -37,9 +37,7 @@ public class SkiaEncoder : IImageEncoder /// </summary> 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 /// <summary> /// Gets the default typeface to use. /// </summary> - public static SKTypeface DefaultTypeFace => _typefaces.Last(); + public static SKTypeface? DefaultTypeFace => _typefaces.Last(); /// <summary> /// Check if the native lib is available. @@ -152,6 +135,40 @@ public class SkiaEncoder : IImageEncoder } } + /// <summary> + /// 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) + /// </summary> + /// <returns>The list of typefaces.</returns> + 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<SKTypeface>(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(); + } + /// <summary> /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>. /// </summary> @@ -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 <Ghost_of_Stone@web.de> Date: Mon, 30 Mar 2026 00:12:35 +0200 Subject: [PATCH 319/390] 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 /// <inheritdoc /> 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); + } } /// <inheritdoc /> 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 /// <param name="itemId">The item whose data is being pruned.</param> /// <param name="cancellationToken">Abort token.</param> /// <returns>A task representing the asynchronous cleanup operation.</returns> - 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 <karivika@hotmail.com> Date: Sun, 29 Mar 2026 14:17:38 -0400 Subject: [PATCH 320/390] 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 <dkanada@users.noreply.github.com> Date: Tue, 31 Mar 2026 01:41:47 +0900 Subject: [PATCH 321/390] 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 322/390] Backport pull request #16425 from jellyfin/release-10.11.z Fix restore backup metadata location Original-merge: 0f1732e5f5444cd6876dec816b5ff5822a93862b Merged-by: joshuaboniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com> --- .../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 <Molier@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:08:05 -0400 Subject: [PATCH 323/390] 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 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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<string>(); 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<string>(); 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 324/390] Backport pull request #16514 from jellyfin/release-10.11.z Fix lint issue Original-merge: e1691e649e8431d080e1d1bc0aacbc2e6198f371 Merged-by: joshuaboniface <joshua@boniface.me> Backported-by: Bond_009 <bond.009@outlook.com> --- 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 /// <summary> /// 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). /// </summary> /// <returns>The list of typefaces.</returns> private static SKTypeface?[] InitializeTypefaces() From 5b3537b3d7260fb0ab0694d68ddc03e5495f262d Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Mon, 30 Mar 2026 14:08:07 -0400 Subject: [PATCH 325/390] 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 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../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 <bond.009@outlook.com> Date: Mon, 30 Mar 2026 14:08:09 -0400 Subject: [PATCH 326/390] Backport pull request #16522 from jellyfin/release-10.11.z Fix CA1810 build error Original-merge: 7e88b18192762dcbf82b2182bacd486b4d828e04 Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; private readonly ILogger<SkiaEncoder> _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); /// <summary> /// The default sampling options, equivalent to old high quality filter settings when upscaling. /// </summary> - public static readonly SKSamplingOptions UpscaleSamplingOptions; + public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); /// <summary> /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling. /// </summary> - 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); /// <summary> /// Initializes a new instance of the <see cref="SkiaEncoder"/> class. From b6e4b3a4f5e9079b673885339b2219f94c27f3c7 Mon Sep 17 00:00:00 2001 From: Lofuuzi <lather0519@gmail.com> Date: Mon, 30 Mar 2026 11:04:55 -0400 Subject: [PATCH 327/390] 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 <lather0519@gmail.com> Date: Tue, 31 Mar 2026 11:16:51 -0400 Subject: [PATCH 328/390] 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 <theguymadmax@proton.me> Date: Tue, 31 Mar 2026 22:05:37 -0400 Subject: [PATCH 329/390] 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 <giorgimodebadze499@gmail.com> Date: Tue, 31 Mar 2026 15:45:11 -0400 Subject: [PATCH 330/390] 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 <mhatretush@gmail.com> Date: Thu, 2 Apr 2026 01:49:27 -0400 Subject: [PATCH 331/390] 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 <riks_12@hot.ee> Date: Thu, 2 Apr 2026 07:50:31 -0400 Subject: [PATCH 332/390] 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 <pankov.76.tvink@gmail.com> Date: Fri, 5 Sep 2025 00:21:20 +0300 Subject: [PATCH 333/390] 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 /// <value>The gain required for audio normalization.</value> public float? NormalizationGain { get; set; } + /// <summary> + /// Gets or sets the gain required for audio normalization. This field is inherited from music album normalization gain. + /// </summary> + /// <value>The gain required for audio normalization.</value> + public float? AlbumNormalizationGain { get; set; } + /// <summary> /// Gets or sets the current program. /// </summary> From c5726559fd6b6a6263880e78737a268015836cf6 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Thu, 2 Apr 2026 19:43:53 -0400 Subject: [PATCH 334/390] 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<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music); - } - public override UnratedItem GetBlockUnratedType() { return UnratedItem.Music; From 87c8349c6bcb5fe7b1c31cbaba2ef29b4b9a15f6 Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:52:37 +0900 Subject: [PATCH 335/390] 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 <dkanada@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:17:31 +0900 Subject: [PATCH 336/390] 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 <joachim.huffer@gmail.com> Date: Fri, 3 Apr 2026 05:01:23 -0400 Subject: [PATCH 337/390] 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 <lather0519@gmail.com> Date: Fri, 3 Apr 2026 05:56:02 -0400 Subject: [PATCH 338/390] 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 <lather0519@gmail.com> Date: Fri, 3 Apr 2026 14:51:58 -0400 Subject: [PATCH 339/390] 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 <lednurb@live.nl> Date: Sat, 4 Apr 2026 02:48:03 -0400 Subject: [PATCH 340/390] 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 <dkanada@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:39:15 +0900 Subject: [PATCH 341/390] 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 <gabrielmarfil0@gmail.com> Date: Sat, 4 Apr 2026 08:08:41 -0400 Subject: [PATCH 342/390] 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 <dkanada@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:19:10 +0900 Subject: [PATCH 343/390] 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 <dkanada@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:27:11 +0900 Subject: [PATCH 344/390] 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 /// <summary> /// Gets all persons. /// </summary> + /// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="searchTerm">The search term.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> @@ -57,6 +58,7 @@ public class PersonsController : BaseJellyfinApiController /// <param name="enableImageTypes">Optional. The image types to include in the output.</param> /// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param> /// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param> + /// <param name="parentId">Optional. Specify this to localize the search to a specific library. Omit to use the root.</param> /// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param> /// <param name="userId">User id.</param> /// <param name="enableImages">Optional, include image information in output.</param> @@ -65,6 +67,7 @@ public class PersonsController : BaseJellyfinApiController [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<BaseItemDto>> 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<JellyfinDbContext> 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<JellyfinDbContext> 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; } + /// <summary> /// Gets or sets the maximum number of items the query should return. /// </summary> @@ -28,6 +30,8 @@ namespace MediaBrowser.Controller.Entities public Guid ItemId { get; set; } + public Guid? ParentId { get; set; } + public IReadOnlyList<string> PersonTypes { get; } public IReadOnlyList<string> ExcludePersonTypes { get; } From cc7bfff41207a9756a00f73f22d8d6229b089ebd Mon Sep 17 00:00:00 2001 From: lednurb <lednurb@live.nl> Date: Sun, 5 Apr 2026 03:30:13 -0400 Subject: [PATCH 345/390] 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 <lather0519@gmail.com> Date: Sun, 5 Apr 2026 02:18:43 -0400 Subject: [PATCH 346/390] 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 <noreply@weblate.org> Date: Sun, 5 Apr 2026 05:36:43 -0400 Subject: [PATCH 347/390] 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 348/390] 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 @@ <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" /> <PackageVersion Include="System.Text.Json" Version="10.0.5" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.11.0" /> + <PackageVersion Include="z440.atl.core" Version="7.12.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> From 34e2f91d502fc9034591836947e4351d44aa9f12 Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Sun, 5 Apr 2026 16:35:15 -0400 Subject: [PATCH 349/390] 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 <lather0519@gmail.com> Date: Sun, 5 Apr 2026 12:34:45 -0400 Subject: [PATCH 350/390] 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 351/390] 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 <karpenko.serhii@outlook.com> Date: Mon, 6 Apr 2026 04:49:56 -0400 Subject: [PATCH 352/390] 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 <karpenko.serhii@outlook.com> Date: Mon, 6 Apr 2026 04:50:01 -0400 Subject: [PATCH 353/390] 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 354/390] Backport pull request #16539 from jellyfin/release-10.11.z Fix subtitle saving Original-merge: f51c63e244436944d5269085a1bed1e56db7a78e Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- 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<string> savePaths, Video video, string extension) { + if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Invalid subtitle format: {extension}"); + } + List<Exception>? exs = null; foreach (var savePath in savePaths) From c008f28d3126186e0a646121a3f69bd1624e37f5 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:19:33 -0400 Subject: [PATCH 355/390] Backport pull request #16540 from jellyfin/release-10.11.z Handle folders without associated library in FixLibrarySubtitleDownloadLanguages Original-merge: be095f85ab80db1d20fccba8774856abe9ae0bd1 Merged-by: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Backported-by: Bond_009 <bond.009@outlook.com> --- .../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" <joshua@boniface.me> Date: Sun, 29 Mar 2026 17:22:14 -0400 Subject: [PATCH 356/390] Fix GHSA-j2hf-x4q5-47j3 with improved sanitization Co-Authored-By: Shadowghost <Ghost_of_Stone@web.de> --- 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); + } } /// <summary> 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<string> _allowedSubtitleFormats; private readonly ISubtitleProvider[] _subtitleProviders; @@ -41,7 +43,8 @@ namespace MediaBrowser.Providers.Subtitles ILibraryMonitor monitor, IMediaSourceManager mediaSourceManager, ILocalizationManager localizationManager, - IEnumerable<ISubtitleProvider> subtitleProviders) + IEnumerable<ISubtitleProvider> 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<string>( + namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')), + StringComparer.OrdinalIgnoreCase); } /// <inheritdoc /> @@ -171,6 +177,12 @@ namespace MediaBrowser.Providers.Subtitles /// <inheritdoc /> 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" <joshua@boniface.me> Date: Sun, 29 Mar 2026 17:30:09 -0400 Subject: [PATCH 357/390] Fix GHSA-8fw7-f233-ffr8 with improved sanitization Co-Authored-By: Shadowghost <Ghost_of_Stone@web.de> --- 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 <Ghost_of_Stone@web.de> Date: Mon, 30 Mar 2026 09:40:01 +0200 Subject: [PATCH 358/390] 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; /// <summary> @@ -17,5 +19,6 @@ public class NewGroupRequestDto /// Gets or sets the group name. /// </summary> /// <value>The name of the new group.</value> + [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 <Ghost_of_Stone@web.de> Date: Mon, 30 Mar 2026 10:48:51 +0200 Subject: [PATCH 359/390] 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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); + } + /// <summary> /// Parses the container into its file extension. /// </summary> 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 { /// <summary> - /// 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. /// </summary> - public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; /// <summary> /// 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, } + /// <summary> + /// 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. + /// </summary> + [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<string>(); - 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 <Ghost_of_Stone@web.de> Date: Tue, 31 Mar 2026 09:30:45 +0200 Subject: [PATCH 360/390] 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}$"; /// <summary> - /// The level validation regex. + /// The level validation regex string. /// This regular expression matches strings representing a double. /// </summary> - 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. /// </summary> - [GeneratedRegex(@"^[a-zA-Z0-9\-\._,|]{0,40}$")] + [GeneratedRegex(ContainerValidationRegexStr)] public static partial Regex ContainerValidationRegex(); + /// <summary> + /// The level validation regex string. + /// This regular expression matches strings representing a double. + /// </summary> + [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 <Ghost_of_Stone@web.de> Date: Tue, 31 Mar 2026 16:35:15 +0200 Subject: [PATCH 361/390] 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 /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] public async Task<ActionResult> ResetTuner([FromRoute, Required] string tunerId) { await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); @@ -976,7 +976,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Created tuner host returned.</response> /// <returns>A <see cref="OkResult"/> containing the created tuner host.</returns> [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); @@ -988,7 +988,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="204">Tuner host deleted.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [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 /// <response code="200">Created listings provider returned.</response> /// <returns>A <see cref="OkResult"/> containing the created listings provider.</returns> [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<ActionResult<ListingsProviderInfo>> AddListingProvider( @@ -1047,7 +1047,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="204">Listing provider deleted.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [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 /// <response code="200">Available countries returned.</response> /// <returns>A <see cref="FileResult"/> containing the available countries.</returns> [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task<ActionResult> GetSchedulesDirectCountries() @@ -1101,7 +1101,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Channel mapping options returned.</response> /// <returns>An <see cref="OkResult"/> containing the channel mapping options.</returns> [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId) => _listingsManager.GetChannelMappingOptions(providerId); @@ -1113,7 +1113,7 @@ public class LiveTvController : BaseJellyfinApiController /// <response code="200">Created channel mapping returned.</response> /// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns> [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto) => _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId); @@ -1137,7 +1137,7 @@ public class LiveTvController : BaseJellyfinApiController /// <returns>An <see cref="OkResult"/> containing the tuners.</returns> [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public IAsyncEnumerable<TunerHostInfo> 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 <lather0519@gmail.com> Date: Mon, 6 Apr 2026 15:34:18 -0400 Subject: [PATCH 362/390] 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 <h41822@gmail.com> Date: Tue, 7 Apr 2026 19:39:13 -0400 Subject: [PATCH 363/390] 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 364/390] 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 @@ <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Moq" Version="4.18.4" /> From 553f38a2377cf843404cd4d3b3602e8a72bc75f8 Mon Sep 17 00:00:00 2001 From: Lasath Fernando <devel@lasath.org> Date: Sat, 4 Apr 2026 16:10:07 +0000 Subject: [PATCH 365/390] Fix language display for ISO 639-2-only codes (e.g. mul, und) LoadCultures() in LocalizationManager skipped all iso6392.txt entries without a two-letter ISO 639-1 code, dropping 302 of 496 languages including mul (Multiple languages), und (Undetermined), mis (Uncoded languages), zxx, and many real languages like Achinese, Akkadian, etc. This caused FindLanguageInfo() to return null for these codes, which meant: - ExternalPathParser could not recognize them as valid language codes in subtitle filenames, so the Language field was never set - DisplayTitle fell back to the raw code string (e.g. "Mul") Fix by allowing entries without two-letter codes to be loaded with an empty TwoLetterISOLanguageName. Also set LocalizedLanguage in ProbeResultNormalizer for ffprobe-detected streams (the DB repository path was already handled on master). --- .../Localization/LocalizationManager.cs | 2 +- .../Probing/ProbeResultNormalizer.cs | 6 ++++++ MediaBrowser.Model/Entities/MediaStream.cs | 1 - .../Localization/LocalizationManagerTests.cs | 21 ++++++++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index bc80c2b405..6fca5bc1ba 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization string twoCharName = parts[2]; if (string.IsNullOrWhiteSpace(twoCharName)) { - continue; + twoCharName = string.Empty; } else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index d3e7b52315..203e72de36 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,6 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); + stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) + ? _localization.FindLanguageInfo(stream.Language)?.DisplayName + : null; stream.Channels = streamInfo.Channels; @@ -767,6 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); + stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) + ? _localization.FindLanguageInfo(stream.Language)?.DisplayName + : null; if (string.IsNullOrEmpty(stream.Title)) { diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 11f81ff7d8..4491fb5ace 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Text; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index e60522bf78..700ac5dced 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var cultures = localizationManager.GetCultures().ToList(); - Assert.Equal(194, cultures.Count); + Assert.Equal(496, cultures.Count); var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal)); Assert.NotNull(germany); @@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Contains("ger", germany.ThreeLetterISOLanguageNames); } + [Theory] + [InlineData("mul", "Multiple languages")] + [InlineData("und", "Undetermined")] + [InlineData("mis", "Uncoded languages")] + [InlineData("zxx", "No linguistic content; Not applicable")] + public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName) + { + var localizationManager = Setup(new ServerConfiguration + { + UICulture = "en-US" + }); + await localizationManager.LoadAll(); + + var culture = localizationManager.FindLanguageInfo(code); + Assert.NotNull(culture); + Assert.Equal(expectedDisplayName, culture.DisplayName); + Assert.Equal(code, culture.ThreeLetterISOLanguageName); + } + [Fact] public async Task GetParentalRatings_Default_Success() { From c300651d0ddae2e2a3b3cde55989284f5972a79a Mon Sep 17 00:00:00 2001 From: Lasath Fernando <devel@lasath.org> Date: Thu, 9 Apr 2026 13:53:38 -0500 Subject: [PATCH 366/390] Simplify null-check ternary style in ProbeResultNormalizer Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../Probing/ProbeResultNormalizer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 203e72de36..3c6a03713f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,9 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); - stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) - ? _localization.FindLanguageInfo(stream.Language)?.DisplayName - : null; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; stream.Channels = streamInfo.Channels; @@ -770,9 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); - stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) - ? _localization.FindLanguageInfo(stream.Language)?.DisplayName - : null; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; if (string.IsNullOrEmpty(stream.Title)) { From 22f050725878661d475b63745656a5b9d57dafe5 Mon Sep 17 00:00:00 2001 From: Dominic DeMarco <awesome3165@gmail.com> Date: Fri, 10 Apr 2026 00:21:52 -0700 Subject: [PATCH 367/390] Record missing information Fixes tonemapping checks by recording previously missing information when gathering video stream information from videos in a BDMV structure --- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index bde23e842f..a89f059060 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -366,6 +366,8 @@ namespace MediaBrowser.Providers.MediaInfo blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace; blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer; blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries; + blurayVideoStream.BitDepth = ffmpegVideoStream.BitDepth; + blurayVideoStream.PixelFormat = ffmpegVideoStream.PixelFormat; } } From 29d11f6ecb529ca091f6538ea3dc6c9a0fd6d4c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:09:15 +0000 Subject: [PATCH 368/390] Update actions/upload-artifact action to v7.0.1 --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/openapi-generate.yml | 2 +- .github/workflows/openapi-pull-request.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index f9e2fbc3a6..dd48209a1f 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: abi-base retention-days: 14 diff --git a/.github/workflows/openapi-generate.yml b/.github/workflows/openapi-generate.yml index 255cc49e82..dbfaf9d30b 100644 --- a/.github/workflows/openapi-generate.yml +++ b/.github/workflows/openapi-generate.yml @@ -36,7 +36,7 @@ jobs: 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 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.artifact }} path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index 563a0a406f..4acd0f4d4f 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -74,7 +74,7 @@ jobs: 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 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: openapi-report path: /tmp/openapi-report/openapi-report.md From 6fc406f2c58e9dafc10f28d9c0e4eeb68283e26c Mon Sep 17 00:00:00 2001 From: Lofuuzi <lather0519@gmail.com> Date: Sat, 11 Apr 2026 19:22:03 -0400 Subject: [PATCH 369/390] 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 8ae899a73c..0a454b2938 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -29,7 +29,7 @@ "Inherit": "繼承", "ItemAddedWithName": "{0} 經已加咗入媒體櫃", "ItemRemovedWithName": "{0} 經已由媒體櫃移除咗", - "LabelIpAddressValue": "IP 地址:{0}", + "LabelIpAddressValue": "IP 位址:{0}", "LabelRunningTimeValue": "運行時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 經已更新咗", From 22644075e784ccd71d39a27eae6d1f7434f47a00 Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:42:49 +0900 Subject: [PATCH 370/390] add NameStartsWith and NameLessThan filters to Person search --- Jellyfin.Api/Controllers/PersonsController.cs | 6 ++++++ .../Item/PeopleRepository.cs | 10 ++++++++++ .../Entities/InternalPeopleQuery.cs | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 2b2afb0fe6..a113ded049 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -50,6 +50,8 @@ public class PersonsController : BaseJellyfinApiController /// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="searchTerm">The search term.</param> + /// <param name="nameStartsWith">Optional. Filter by items whose name starts with the given input string.</param> + /// <param name="nameLessThan">Optional. Filter by items whose name will appear before this value when sorted alphabetically.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="filters">Optional. Specify additional filters to apply.</param> /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param> @@ -70,6 +72,8 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, @@ -97,6 +101,8 @@ public class PersonsController : BaseJellyfinApiController excludePersonTypes) { NameContains = searchTerm, + NameStartsWith = nameStartsWith, + NameLessThan = nameLessThan, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index ad9953d1b6..adb5e08cf9 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -235,6 +235,16 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper)); } + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + { + query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant())); + } + + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) + { + query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0); + } + return query; } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index f4b3910b0e..373ec7ffeb 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -42,6 +42,10 @@ namespace MediaBrowser.Controller.Entities public string NameContains { get; set; } + public string NameStartsWith { get; set; } + + public string NameLessThan { get; set; } + public User User { get; set; } public bool? IsFavorite { get; set; } From bb265cd4030e966aec1afbd31eafc0973531c232 Mon Sep 17 00:00:00 2001 From: dkanada <dkanada@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:50:04 +0900 Subject: [PATCH 371/390] add NameStartsWithOrGreater parameter to Persons endpoint --- Jellyfin.Api/Controllers/PersonsController.cs | 3 +++ Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 5 +++++ MediaBrowser.Controller/Entities/InternalPeopleQuery.cs | 2 ++ 3 files changed, 10 insertions(+) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index a113ded049..1811a219ac 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -52,6 +52,7 @@ public class PersonsController : BaseJellyfinApiController /// <param name="searchTerm">The search term.</param> /// <param name="nameStartsWith">Optional. Filter by items whose name starts with the given input string.</param> /// <param name="nameLessThan">Optional. Filter by items whose name will appear before this value when sorted alphabetically.</param> + /// <param name="nameStartsWithOrGreater">Optional. Filter by items whose name will appear after this value when sorted alphabetically.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="filters">Optional. Specify additional filters to apply.</param> /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param> @@ -74,6 +75,7 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] string? searchTerm, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery] string? nameStartsWithOrGreater, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, @@ -103,6 +105,7 @@ public class PersonsController : BaseJellyfinApiController NameContains = searchTerm, NameStartsWith = nameStartsWith, NameLessThan = nameLessThan, + NameStartsWithOrGreater = nameStartsWithOrGreater, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index adb5e08cf9..7147fbfe7d 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -245,6 +245,11 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0); } + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) + { + query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0); + } + return query; } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 373ec7ffeb..e12ba22343 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -46,6 +46,8 @@ namespace MediaBrowser.Controller.Entities public string NameLessThan { get; set; } + public string NameStartsWithOrGreater { get; set; } + public User User { get; set; } public bool? IsFavorite { get; set; } From ce3fa80a28a660fbfed632b6e556c2a77c7b0620 Mon Sep 17 00:00:00 2001 From: Gargotaire <gargots@gmail.com> Date: Mon, 13 Apr 2026 13:15:41 -0400 Subject: [PATCH 372/390] 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 fce3a614c2..ec5cbf0d43 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} s'ha instal·lat", - "PluginUninstalledWithName": "{0} s'ha desinstal·lat", + "PluginInstalledWithName": "S'ha instal·lat {0}", + "PluginUninstalledWithName": "S'ha desinstal·lat {0}", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", From fb33b725e064f0954c5a4f2af17041b7552eb6e1 Mon Sep 17 00:00:00 2001 From: Tim Eisele <Ghost_of_Stone@web.de> Date: Mon, 13 Apr 2026 20:06:46 +0200 Subject: [PATCH 373/390] Fix in-process restart (#16482) Fix in-process restart --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 1 + Jellyfin.Server/Program.cs | 3 ++- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 7cff2a25b6..23bd5cf200 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -60,6 +60,7 @@ namespace Emby.Server.Implementations.IO _fileSystem = fileSystem; appLifetime.ApplicationStarted.Register(Start); + appLifetime.ApplicationStopping.Register(Stop); } /// <inheritdoc /> diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 93f71fdc69..93ba672535 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -161,7 +161,6 @@ namespace Jellyfin.Server _loggerFactory, options, startupConfig); - _appHost = appHost; var configurationCompleted = false; try { @@ -207,6 +206,7 @@ namespace Jellyfin.Server await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false); await appHost.InitializeServices(startupConfig).ConfigureAwait(false); + _appHost = appHost; await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.AppInitialisation, appHost.ServiceProvider).ConfigureAwait(false); await jellyfinMigrationService.CleanupSystemAfterMigration(_logger).ConfigureAwait(false); @@ -263,6 +263,7 @@ namespace Jellyfin.Server _appHost = null; _jellyfinHost?.Dispose(); + _jellyfinHost = null; } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 1aa39f97b6..05975929db 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -142,6 +142,7 @@ public sealed class SetupServer : IDisposable ThrowIfDisposed(); var retryAfterValue = TimeSpan.FromSeconds(5); var config = _configurationManager.GetNetworkConfiguration()!; + _startupServer?.Dispose(); _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"]) .UseConsoleLifetime() .UseSerilog() From 5bad7b8ae324ccf0d4eb3051d0944edfddfdb50b Mon Sep 17 00:00:00 2001 From: theguymadmax <theguymadmax@proton.me> Date: Tue, 14 Apr 2026 12:38:01 -0400 Subject: [PATCH 374/390] Fix artist metadata not being fetched on initial library scan (#16606) * Fix artist metadata not being fetched on initial library scan * Update Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs Co-authored-by: Bond-009 <bond.009@outlook.com> --------- Co-authored-by: Bond-009 <bond.009@outlook.com> --- .../Library/Validators/ArtistsValidator.cs | 11 ++++++++++- MediaBrowser.Providers/Manager/ProviderManager.cs | 7 +++++++ .../Plugins/AudioDb/AudioDbArtistProvider.cs | 6 +++++- .../Plugins/MusicBrainz/MusicBrainzArtistProvider.cs | 6 +++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7cc851b73b..ef20ae9bca 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -50,6 +50,10 @@ public class ArtistsValidator public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) { var names = _itemRepo.GetAllArtistNames(); + var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist] + }).ToHashSet(); var numComplete = 0; var count = names.Count; @@ -59,8 +63,13 @@ public class ArtistsValidator try { var item = _libraryManager.GetArtist(name); + var isNew = !existingArtistIds.Contains(item.Id); + var neverRefreshed = item.DateLastRefreshed == default; - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + if (isNew || neverRefreshed) + { + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException) { diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f8e2aece1f..0bab73180f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -487,6 +487,13 @@ namespace MediaBrowser.Providers.Manager return true; } + // Artists without a folder structure that are derived from metadata have no real path in the library, + // so GetLibraryOptions returns null. Allow all providers through rather than blocking them. + if (item is MusicArtist && libraryTypeOptions is null) + { + return true; + } + return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 00bd96282c..d8cb6b4b24 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -125,7 +125,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (string.IsNullOrWhiteSpace(overview)) { - overview = result.strBiographyEN; + overview = string.IsNullOrWhiteSpace(result.strBiographyEN) + ? result.strBiography + : result.strBiographyEN; } item.Overview = (overview ?? string.Empty).StripHtml(); @@ -224,6 +226,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string strTwitter { get; set; } + public string strBiography { get; set; } + public string strBiographyEN { get; set; } public string strBiographyDE { get; set; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 1323d2604a..9df21596c5 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// <summary> /// MusicBrainz artist provider. /// </summary> -public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable +public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable, IHasOrder { private readonly ILogger<MusicBrainzArtistProvider> _logger; private Query _musicBrainzQuery; @@ -42,6 +42,10 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar /// <inheritdoc /> public string Name => "MusicBrainz"; + /// <inheritdoc /> + /// Runs first to populate the MusicBrainz artist ID used by downstream providers. + public int Order => 0; + private void ReloadConfig(object? sender, BasePluginConfiguration e) { var configuration = (PluginConfiguration)e; From 1ac5f71bf3cecce91c05f98b338d136833c1b672 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:21:13 +0000 Subject: [PATCH 375/390] Update dependency dotnet-ef to v10.0.6 --- .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 9cd9c08e75..3f7a9454c5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.5", + "version": "10.0.6", "commands": [ "dotnet-ef" ] From f9c7b18fdd8f7dfd46e642d0a829436fd352d6db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:21:22 +0000 Subject: [PATCH 376/390] Update Microsoft to 10.0.6 --- Directory.Packages.props | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bf79ff0e0e..9b0d6f93c5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,27 +26,27 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.5" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.6" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.6" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" /> <PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.6" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.6" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> @@ -77,7 +77,7 @@ <PackageVersion Include="Svg.Skia" Version="3.4.1" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" /> - <PackageVersion Include="System.Text.Json" Version="10.0.5" /> + <PackageVersion Include="System.Text.Json" Version="10.0.6" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.12.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> From fa8073d9ff7fbfea29970fd1762f4723f6ee556b Mon Sep 17 00:00:00 2001 From: Sakari Kukkonen <sakari.j.kukkonen@gmail.com> Date: Tue, 14 Apr 2026 19:10:10 -0400 Subject: [PATCH 377/390] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- Emby.Server.Implementations/Localization/Core/fi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 15a04d22cd..79afbb519b 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -39,8 +39,8 @@ "Channels": "Kanavat", "CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}", "Books": "Kirjat", - "AuthenticationSucceededWithUserName": "{0} on todennettu", - "Artists": "Esittäjät", + "AuthenticationSucceededWithUserName": "{0} todennus onnistunut", + "Artists": "Artistit", "Application": "Sovellus", "AppDeviceValues": "Sovellus: {0}, Laite: {1}", "Albums": "Albumit", From 03523be5555ba76171dafe367d0606570424a2f6 Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Wed, 15 Apr 2026 09:06:12 -0400 Subject: [PATCH 378/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 94db435715..37a1e70240 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "Omiljene serije", "HeaderFavoriteSongs": "Omiljene pjesme", "HeaderLiveTV": "TV uživo", - "HeaderNextUp": "Slijedi", + "HeaderNextUp": "Sljedeće na redu", "HeaderRecordingGroups": "Grupa snimka", "HomeVideos": "Kućni video", "Inherit": "Naslijedi", @@ -73,10 +73,10 @@ "Shows": "Emisije", "Songs": "Pjesme", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", - "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", + "SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}", "Sync": "Sinkronizacija", "System": "Sustav", - "TvShows": "Serije", + "TvShows": "TV emisije", "User": "Korisnik", "UserCreatedWithName": "Korisnik {0} je kreiran", "UserDeletedWithName": "Korisnik {0} je obrisan", @@ -99,8 +99,8 @@ "TaskCleanCache": "Očisti mapu predmemorije", "TasksApplicationCategory": "Aplikacija", "TasksMaintenanceCategory": "Održavanje", - "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.", - "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje", + "TaskDownloadMissingSubtitlesDescription": "Pretraži internet za nedsotajućim titlovima ne osnovi konfiguracije metapodataka.", + "TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove", "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.", "TaskRefreshChannels": "Osvježi kanale", "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.", From ac6edd40c8fcccbc05ca47aceed815795a8b8283 Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Wed, 15 Apr 2026 09:44:43 -0400 Subject: [PATCH 379/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 37a1e70240..c748d68a4d 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -89,7 +89,7 @@ "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", - "ValueSpecialEpisodeName": "Posebno - {0}", + "ValueSpecialEpisodeName": "Posebno – {0}", "VersionNumber": "Verzija {0}", "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.", "TaskRefreshLibrary": "Skeniraj medijsku biblioteku", From 1864c174128052b1acdbe3dc132c56c619b88fed Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Wed, 15 Apr 2026 10:20:46 -0400 Subject: [PATCH 380/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index c748d68a4d..b1d3548e9b 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -119,7 +119,7 @@ "Forced": "Forsirani", "Default": "Zadano", "TaskOptimizeDatabase": "Optimiziraj bazu podataka", - "External": "Vanjski", + "External": "Eksterni", "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "TaskKeyframeExtractor": "Izvoditelj ključnog okvira", "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.", From 8a01f04de50970f205462e6ea1bfc96fc7573d0e Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Wed, 15 Apr 2026 10:26:32 -0400 Subject: [PATCH 381/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index b1d3548e9b..289145f670 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -88,11 +88,11 @@ "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}", "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}", - "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", + "ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija", "ValueSpecialEpisodeName": "Posebno – {0}", "VersionNumber": "Verzija {0}", - "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.", - "TaskRefreshLibrary": "Skeniraj medijsku biblioteku", + "TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.", + "TaskRefreshLibrary": "Skeniraj biblioteku medija", "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.", "TaskRefreshChapterImages": "Izdvoji slike poglavlja", "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.", @@ -107,7 +107,7 @@ "TaskCleanTranscode": "Očisti mapu transkodiranja", "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.", "TaskUpdatePlugins": "Ažuriraj dodatke", - "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.", + "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u biblioteci medija.", "TaskRefreshPeople": "Osvježi osobe", "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.", "TaskCleanLogs": "Očisti mapu dnevnika zapisa", From 9239b1211885d5d4b7fec0c9b61baac9ad4778e5 Mon Sep 17 00:00:00 2001 From: Milo Ivir <mail@milotype.de> Date: Wed, 15 Apr 2026 10:27:45 -0400 Subject: [PATCH 382/390] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 289145f670..eaeb173c20 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -135,7 +135,7 @@ "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama", "TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.", "TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja", - "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.", + "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.", "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka", "CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana." } From 8ba9319f27bb6d2e93640b9c8426c4e6e9470133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hilmar=20G=C3=BAstafsson?= <LiHRaM@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:11:29 +0200 Subject: [PATCH 383/390] fix: retain subtitles spanning HLS segment boundaries (#16594) fix: retain subtitles spanning HLS segment boundaries --- CONTRIBUTORS.md | 1 + .../Subtitles/SubtitleEncoder.cs | 10 +- .../Subtitles/FilterEventsTests.cs | 282 ++++++++++++++++++ 3 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b0cec3c92..01f968690f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -212,6 +212,7 @@ - [martenumberto](https://github.com/martenumberto) - [ZeusCraft10](https://github.com/ZeusCraft10) - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) + - [LiHRaM](https://github.com/LiHRaM) # Emby Contributors diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9aeac7221e..5920fe3289 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -101,11 +101,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles return ms; } - private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) + internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) { - // Drop subs that are earlier than what we're looking for + // Drop subs that have fully elapsed before the requested start position track.TrackEvents = track.TrackEvents - .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0) + .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0) .ToArray(); if (endTimeTicks > 0) @@ -119,8 +119,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles { foreach (var trackEvent in track.TrackEvents) { - trackEvent.EndPositionTicks -= startPositionTicks; - trackEvent.StartPositionTicks -= startPositionTicks; + trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks); + trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs new file mode 100644 index 0000000000..5f84e85592 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs @@ -0,0 +1,282 @@ +using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using MediaBrowser.MediaEncoding.Subtitles; +using MediaBrowser.Model.MediaInfo; +using Xunit; + +namespace Jellyfin.MediaEncoding.Subtitles.Tests +{ + public class FilterEventsTests + { + private readonly SubtitleEncoder _encoder; + + public FilterEventsTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + _encoder = fixture.Create<SubtitleEncoder>(); + } + + [Fact] + public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained() + { + // Subtitle starts at 5s, ends at 15s. + // Segment requested from 10s to 20s. + // The subtitle is still on screen at 10s and should NOT be dropped. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Still on screen") + { + StartPositionTicks = TimeSpan.FromSeconds(5).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("2", "Next subtitle") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(17).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Equal(2, track.TrackEvents.Count); + Assert.Equal("1", track.TrackEvents[0].Id); + Assert.Equal("2", track.TrackEvents[1].Id); + } + + [Fact] + public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped() + { + // Subtitle starts at 2s, ends at 5s. + // Segment requested from 10s. + // The subtitle ended before the segment — should be dropped. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Already gone") + { + StartPositionTicks = TimeSpan.FromSeconds(2).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(5).Ticks + }, + new SubtitleTrackEvent("2", "Visible") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(17).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Single(track.TrackEvents); + Assert.Equal("2", track.TrackEvents[0].Id); + } + + [Fact] + public void FilterEvents_SubtitleAfterSegment_IsDropped() + { + // Segment is 10s-20s, subtitle starts at 25s. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "In range") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("2", "After segment") + { + StartPositionTicks = TimeSpan.FromSeconds(25).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(30).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Single(track.TrackEvents); + Assert.Equal("1", track.TrackEvents[0].Id); + } + + [Fact] + public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps() + { + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Subtitle") + { + StartPositionTicks = TimeSpan.FromSeconds(15).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(20).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(30).Ticks, + preserveTimestamps: false); + + Assert.Single(track.TrackEvents); + // Timestamps should be shifted back by 10s + Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks); + Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks); + } + + [Fact] + public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps() + { + var startTicks = TimeSpan.FromSeconds(15).Ticks; + var endTicks = TimeSpan.FromSeconds(20).Ticks; + + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Subtitle") + { + StartPositionTicks = startTicks, + EndPositionTicks = endTicks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(30).Ticks, + preserveTimestamps: true); + + Assert.Single(track.TrackEvents); + Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks); + Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks); + } + + [Fact] + public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained() + { + // Subtitle ends exactly when the segment begins. + // EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0, + // so SkipWhile stops and the subtitle is retained. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Boundary subtitle") + { + StartPositionTicks = TimeSpan.FromSeconds(5).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(10).Ticks + }, + new SubtitleTrackEvent("2", "In range") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Equal(2, track.TrackEvents.Count); + Assert.Equal("1", track.TrackEvents[0].Id); + } + + [Fact] + public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps() + { + // Subtitle starts at 5s, ends at 15s. + // Segment requested from 10s to 20s, preserveTimestamps = false. + // The subtitle spans the boundary and is retained, but shifting + // StartPositionTicks by -10s would produce -5s (negative). + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Spans boundary") + { + StartPositionTicks = TimeSpan.FromSeconds(5).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("2", "Fully in range") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(17).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: false); + + Assert.Equal(2, track.TrackEvents.Count); + // Subtitle 1: start should be clamped to 0, not -5s + Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative"); + Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks); + // Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s) + Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks); + Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks); + } + + [Fact] + public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition() + { + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Before") + { + StartPositionTicks = TimeSpan.FromSeconds(2).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(4).Ticks + }, + new SubtitleTrackEvent("2", "After") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("3", "Much later") + { + StartPositionTicks = TimeSpan.FromSeconds(500).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(505).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: 0, + preserveTimestamps: true); + + Assert.Equal(2, track.TrackEvents.Count); + Assert.Equal("2", track.TrackEvents[0].Id); + Assert.Equal("3", track.TrackEvents[1].Id); + } + } +} From eb6c0e093552974d09476135ce95501ef01f5c46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:46:47 +0200 Subject: [PATCH 384/390] Update github/codeql-action action to v4.35.2 (#16639) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .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 f6f9a26d06..442114dd80 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -32,13 +32,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 From 8510cfe77de66d12bc88fcbef4b06ca637d90ab8 Mon Sep 17 00:00:00 2001 From: Eliya Wolfram Konzo <ewkonzo@gmail.com> Date: Wed, 15 Apr 2026 20:59:13 +0300 Subject: [PATCH 385/390] Added Tanzania countries.json (#14406) * Update countries.json Added Tanzania * Fix order countries.json * Fix tests --------- Co-authored-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Localization/countries.json | 6 ++++++ .../Localization/LocalizationManagerTests.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index d92dc880b1..811a7d4094 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -749,6 +749,12 @@ "ThreeLetterISORegionName": "TAJ", "TwoLetterISORegionName": "TJ" }, + { + "DisplayName": "Tanzania", + "Name": "TZ", + "ThreeLetterISORegionName": "TZA", + "TwoLetterISORegionName": "TZ" + }, { "DisplayName": "Thailand", "Name": "TH", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 700ac5dced..5bcfc580ff 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization }); var countries = localizationManager.GetCountries().ToList(); - Assert.Equal(139, countries.Count); + Assert.Equal(140, countries.Count); var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal)); Assert.NotNull(germany); From d8c55a29bb0f3fe04ba083bb338ca80cf7f1601e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:59:35 +0000 Subject: [PATCH 386/390] Update danielpalme/ReportGenerator-GitHub-Action action to v5.5.5 --- .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 fc32cc884d..9fdcaedf97 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@cf6fe1b38ed5becc89ffe056c1f240825993be5b # v5.5.4 + uses: danielpalme/ReportGenerator-GitHub-Action@7ae927204961589fcb0b0be245c51fbbc87cbca2 # v5.5.5 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From 1e64e4284224949c3029ec8f1e3bc66e364da6dd Mon Sep 17 00:00:00 2001 From: SecularSteve <fairfull.playing@gmail.com> Date: Fri, 17 Apr 2026 03:31:38 -0400 Subject: [PATCH 387/390] Added translation using Weblate (Bosnian) --- Emby.Server.Implementations/Localization/Core/bs.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/bs.json diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/bs.json @@ -0,0 +1 @@ +{} From 946ff12185289b5dc38732497bb362aab7f192fe Mon Sep 17 00:00:00 2001 From: SecularSteve <fairfull.playing@gmail.com> Date: Fri, 17 Apr 2026 06:26:31 -0400 Subject: [PATCH 388/390] Translated using Weblate (Bosnian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bs/ --- .../Localization/Core/bs.json | 142 +++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json index 0967ef424b..72b2a1f693 100644 --- a/Emby.Server.Implementations/Localization/Core/bs.json +++ b/Emby.Server.Implementations/Localization/Core/bs.json @@ -1 +1,141 @@ -{} +{ + "Albums": "Albumi", + "Artists": "Umjetnici", + "Books": "Knjige", + "Channels": "Kanalima", + "Collections": "Zbirke", + "Default": "Zadano", + "Favorites": "Omiljeni", + "Folders": "Mape", + "Genres": "Žanrovi", + "HeaderAlbumArtists": "Umjetnici albuma", + "HeaderContinueWatching": "Nastavi gledati", + "Movies": "Filmovi", + "MusicVideos": "Muzički spotovi", + "Photos": "Slike", + "Playlists": "Plejliste", + "Shows": "Pokazuje", + "Songs": "Pjesme", + "ValueSpecialEpisodeName": "Posebno - {0}", + "AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}", + "Application": "Prijava", + "AuthenticationSucceededWithUserName": "{0} uspješno autentificirano", + "CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}", + "ChapterNameValue": "Poglavlje {0}", + "DeviceOfflineWithName": "{0} se odspojio", + "DeviceOnlineWithName": "{0} je povezan", + "External": "Vanjsko", + "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}", + "Forced": "Prisilno", + "HeaderFavoriteAlbums": "Omiljeni albumi", + "HeaderFavoriteArtists": "Omiljeni umjetnici", + "HeaderFavoriteEpisodes": "Omiljene epizode", + "HeaderFavoriteShows": "Omiljene emisije", + "HeaderFavoriteSongs": "Omiljene pjesme", + "HeaderLiveTV": "TV uživo", + "HeaderNextUp": "Slijedi", + "HeaderRecordingGroups": "Grupe za snimanje", + "HearingImpaired": "Oštećen sluh", + "HomeVideos": "Kućni videozapisi", + "Inherit": "Nasljedi", + "ItemAddedWithName": "{0} je dodan u biblioteku", + "ItemRemovedWithName": "{0} je uklonjen iz biblioteke", + "LabelIpAddressValue": "IP adresa: {0}", + "LabelRunningTimeValue": "Trajanje: {0}", + "Latest": "Posljednje dodano", + "MessageApplicationUpdated": "Jellyfin Server je ažuriran", + "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana", + "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana", + "MixedContent": "Miješani sadržaj", + "Music": "Muzika", + "NameInstallFailed": "{0} instalacija je propala", + "NameSeasonNumber": "Sezona {0}", + "NameSeasonUnknown": "Sezona nepoznata", + "NewVersionIsAvailable": "Dostupna je nova verzija Jellyfin Servera za preuzimanje.", + "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", + "NotificationOptionApplicationUpdateInstalled": "Ažuriranje aplikacije instalirano", + "NotificationOptionAudioPlayback": "Pokrenuto je reproduciranje zvuka", + "NotificationOptionAudioPlaybackStopped": "Zaustavljeno je reproduciranje zvuka", + "NotificationOptionCameraImageUploaded": "Učitana slika s kamere", + "NotificationOptionInstallationFailed": "Neuspjeh instalacije", + "NotificationOptionNewLibraryContent": "Dodan novi sadržaj", + "NotificationOptionPluginError": "Neuspjeh dodatka", + "NotificationOptionPluginInstalled": "Dodatak je instaliran", + "NotificationOptionPluginUninstalled": "Dodatak je deinstaliran", + "NotificationOptionPluginUpdateInstalled": "Ažuriranje dodatka je instalirano", + "NotificationOptionServerRestartRequired": "Potreban je ponovni pokret servera", + "NotificationOptionTaskFailed": "Neuspjeh zakazane zadatke", + "NotificationOptionUserLockedOut": "Korisnik je zaključan", + "NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa", + "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena", + "Plugin": "Plugin", + "PluginInstalledWithName": "{0} je instaliran", + "PluginUninstalledWithName": "{0} je deinstaliran", + "PluginUpdatedWithName": "{0} je ažurirano", + "ProviderValue": "Pružatelj: {0}", + "ScheduledTaskFailedWithName": "{0} nije uspjelo", + "ScheduledTaskStartedWithName": "{0} počelo", + "ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti", + "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.", + "SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}", + "Sync": "Sinkronizacija", + "System": "Sistem", + "TvShows": "TV serije", + "Undefined": "Nedefinirano", + "User": "Korisnik", + "UserCreatedWithName": "Korisnik {0} je kreiran", + "UserDeletedWithName": "Korisnik {0} je izbrisan", + "UserDownloadingItemWithValues": "{0} preuzima {1}", + "UserLockedOutWithName": "Korisnik {0} je zaključan", + "UserOfflineFromDevice": "{0} se odspojio od {1}", + "UserOnlineFromDevice": "{0} je online od {1}", + "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", + "UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}", + "UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}", + "ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku", + "VersionNumber": "Verzija {0}", + "TasksMaintenanceCategory": "Održavanje", + "TasksLibraryCategory": "Biblioteka", + "TasksApplicationCategory": "Prijava", + "TasksChannelsCategory": "Internetski kanali", + "TaskCleanActivityLog": "Očisti dnevnik aktivnosti", + "TaskCleanActivityLogDescription": "Brisanje unosa u dnevnik aktivnosti starijih od konfigurisane starosti.", + "TaskCleanCache": "Očistite direktorij keša", + "TaskCleanCacheDescription": "Brisanje keš datoteka koje sistemu više nisu potrebne.", + "TaskRefreshChapterImages": "Izvadi slike iz poglavlja", + "TaskRefreshChapterImagesDescription": "Stvara minijature za videozapise koji imaju poglavlja.", + "TaskAudioNormalization": "Normalizacija zvuka", + "TaskAudioNormalizationDescription": "Skeneriše datoteke radi podataka za normalizaciju zvuka.", + "TaskRefreshLibrary": "Skenerisati medijsku biblioteku", + "TaskRefreshLibraryDescription": "Skenerira vašu medijsku biblioteku na nove datoteke i osvježava metapodatke.", + "TaskCleanLogs": "Očisti direktorij dnevnika", + "TaskCleanLogsDescription": "Brisanje dnevničkih datoteka starijih od {0} dana.", + "TaskRefreshPeople": "Osvježite ljude", + "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i režisere u vašoj medijskoj biblioteci.", + "TaskRefreshTrickplayImages": "Generirajte Trickplay slike", + "TaskRefreshTrickplayImagesDescription": "Stvara pregled trik-igara za videozapise u omogućenim bibliotekama.", + "TaskUpdatePlugins": "Ažuriraj dodatke", + "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja dodataka koji su konfigurisani da se automatski ažuriraju.", + "TaskCleanTranscode": "Očisti Transcode direktorij", + "TaskCleanTranscodeDescription": "Brisanje transkodiranih datoteka starijih od jednog dana.", + "TaskRefreshChannels": "Osvježi kanale", + "TaskRefreshChannelsDescription": "Osvježava informacije o internetskom kanalu.", + "TaskDownloadMissingLyrics": "Preuzmi nedostajuće tekstove", + "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama", + "TaskDownloadMissingSubtitles": "Preuzmite nedostajuće titlove", + "TaskDownloadMissingSubtitlesDescription": "Pretražuje internet u potrazi za nedostajućim titlovima na osnovu konfiguracije metapodataka.", + "TaskOptimizeDatabase": "Optimizirajte bazu podataka", + "TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.", + "TaskKeyframeExtractor": "Izvađač ključnih sličica", + "TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.", + "TaskCleanCollectionsAndPlaylists": "Očistite kolekcije i playliste", + "TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz kolekcija i playlista koje više ne postoje.", + "TaskExtractMediaSegments": "Analiza medijskog segmenta", + "TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.", + "TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay", + "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.", + "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka", + "CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana." +} From 7113a8c57ccf11930bc769a5a98c819c8e649f9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:00:44 +0000 Subject: [PATCH 389/390] Update dependency coverlet.collector to v10 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b0d6f93c5..34e6a02ff9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> - <PackageVersion Include="coverlet.collector" Version="8.0.1" /> + <PackageVersion Include="coverlet.collector" Version="10.0.0" /> <PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> From 1d44899606110ffd7d984bd7d52b8cd57e18c273 Mon Sep 17 00:00:00 2001 From: KGT1 <kilian.gamn@gmx.de> Date: Sat, 18 Apr 2026 17:06:46 +0200 Subject: [PATCH 390/390] Remove global subtitle configuration (#14957) --- .../Providers/SubtitleConfigurationFactory.cs | 21 --------- .../Providers/SubtitleOptions.cs | 36 --------------- .../MediaInfo/FFProbeVideoInfo.cs | 41 ++--------------- .../MediaInfo/SubtitleScheduledTask.cs | 46 ++++++------------- 4 files changed, 20 insertions(+), 124 deletions(-) delete mode 100644 MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs delete mode 100644 MediaBrowser.Model/Providers/SubtitleOptions.cs diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs deleted file mode 100644 index 0445397ad8..0000000000 --- a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Providers; - -namespace MediaBrowser.Common.Providers -{ - public class SubtitleConfigurationFactory : IConfigurationFactory - { - /// <inheritdoc /> - public IEnumerable<ConfigurationStore> GetConfigurations() - { - yield return new ConfigurationStore() - { - Key = "subtitles", - ConfigurationType = typeof(SubtitleOptions) - }; - } - } -} diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs deleted file mode 100644 index 6ea1e14862..0000000000 --- a/MediaBrowser.Model/Providers/SubtitleOptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Providers -{ - public class SubtitleOptions - { - public SubtitleOptions() - { - DownloadLanguages = Array.Empty<string>(); - - SkipIfAudioTrackMatches = true; - RequirePerfectMatch = true; - } - - public bool SkipIfEmbeddedSubtitlesPresent { get; set; } - - public bool SkipIfAudioTrackMatches { get; set; } - - public string[] DownloadLanguages { get; set; } - - public bool DownloadMovieSubtitles { get; set; } - - public bool DownloadEpisodeSubtitles { get; set; } - - public string OpenSubtitlesUsername { get; set; } - - public string OpenSubtitlesPasswordHash { get; set; } - - public bool IsOpenSubtitleVipAccount { get; set; } - - public bool RequirePerfectMatch { get; set; } - } -} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index a89f059060..fdc2f36469 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Extensions; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -25,7 +24,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -74,7 +72,6 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleResolver = subtitleResolver; _mediaAttachmentRepository = mediaAttachmentRepository; _mediaStreamRepository = mediaStreamRepository; - _mediaStreamRepository = mediaStreamRepository; } public async Task<ItemUpdateType> ProbeVideo<T>( @@ -551,47 +548,19 @@ namespace MediaBrowser.Providers.MediaInfo var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; - var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles"); - var libraryOptions = _libraryManager.GetLibraryOptions(video); - string[] subtitleDownloadLanguages; - bool skipIfEmbeddedSubtitlesPresent; - bool skipIfAudioTrackMatches; - bool requirePerfectMatch; - bool enabled; - - if (libraryOptions.SubtitleDownloadLanguages is null) - { - subtitleDownloadLanguages = subtitleOptions.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; - requirePerfectMatch = subtitleOptions.RequirePerfectMatch; - enabled = (subtitleOptions.DownloadEpisodeSubtitles && - video is Episode) || - (subtitleOptions.DownloadMovieSubtitles && - video is Movie); - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; - enabled = true; - } - - if (enableSubtitleDownloading && enabled) + if (enableSubtitleDownloading && libraryOptions.SubtitleDownloadLanguages is not null) { var downloadedLanguages = await new SubtitleDownloader( _logger, _subtitleManager).DownloadSubtitles( video, currentStreams.Concat(externalSubtitleStreams).ToList(), - skipIfEmbeddedSubtitlesPresent, - skipIfAudioTrackMatches, - requirePerfectMatch, - subtitleDownloadLanguages, + libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent, + libraryOptions.SkipSubtitlesIfAudioTrackMatches, + libraryOptions.RequirePerfectSubtitleMatch, + libraryOptions.SubtitleDownloadLanguages, libraryOptions.DisabledSubtitleFetchers, libraryOptions.SubtitleFetcherOrder, true, diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 1134baf92d..7188e9804e 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -8,14 +8,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Providers; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -57,16 +55,9 @@ namespace MediaBrowser.Providers.MediaInfo public bool IsLogged => true; - private SubtitleOptions GetOptions() - { - return _config.GetConfiguration<SubtitleOptions>("subtitles"); - } - /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - var options = GetOptions(); - var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie }; var dict = new Dictionary<Guid, BaseItem>(); @@ -81,17 +72,14 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.SubtitleDownloadLanguages is null) { - subtitleDownloadLanguages = options.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + // Skip this library if subtitle download languages are not configured + continue; } + subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + foreach (var lang in subtitleDownloadLanguages) { var query = new InternalItemsQuery @@ -144,7 +132,7 @@ namespace MediaBrowser.Providers.MediaInfo try { - await DownloadSubtitles(video as Video, options, cancellationToken).ConfigureAwait(false); + await DownloadSubtitles(video as Video, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -160,7 +148,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - private async Task<bool> DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken) + private async Task<bool> DownloadSubtitles(Video video, CancellationToken cancellationToken) { var mediaStreams = video.GetMediaStreams(); @@ -173,19 +161,15 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.SubtitleDownloadLanguages is null) { - subtitleDownloadLanguages = options.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - requirePerfectMatch = options.RequirePerfectMatch; - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + // Subtitle downloading is not configured for this library + return true; } + subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + var downloadedLanguages = await new SubtitleDownloader( _logger, _subtitleManager).DownloadSubtitles(