From 22ce1f25d0686b6f8a06d590289d50a7ee907845 Mon Sep 17 00:00:00 2001 From: sususu98 Date: Wed, 23 Apr 2025 18:18:38 +0800 Subject: [PATCH 001/300] 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 002/300] 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 003/300] 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 004/300] 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 005/300] 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 006/300] =?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 007/300] 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 008/300] 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 009/300] 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 f58b4860f7d295ea39ec64668e133579ac6154f9 Mon Sep 17 00:00:00 2001 From: desibooklover <desibooklover@gmail.com> Date: Wed, 22 Oct 2025 15:09:11 -0400 Subject: [PATCH 010/300] 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 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 813b18ad4b..80db975ccb 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -129,5 +129,12 @@ "TaskAudioNormalization": "श्रव्य सामान्यीकरण", "TaskAudioNormalizationDescription": "श्रव्य सामान्यीकरण के लिए फाइलें अन्वेषण करें", "TaskDownloadMissingLyrics": "लापता गानों के बोल डाउनलोड करेँ", - "TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है" + "TaskDownloadMissingLyricsDescription": "गानों के बोल डाउनलोड करता है", + "TaskExtractMediaSegments": "मीडिया सेगमेंट स्कैन", + "TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।", + "TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें", + "TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।", + "TaskCleanCollectionsAndPlaylistsDescription": "संग्रहों और प्लेलिस्टों से उन आइटमों को हटाता है जो अब मौजूद नहीं हैं।", + "TaskCleanCollectionsAndPlaylists": "संग्रह और प्लेलिस्ट साफ़ करें", + "CleanupUserDataTask": "यूज़र डेटा की सफाई करता है।" } From d167d59c23765d2cc403fec778b53ea1b3dbe40d Mon Sep 17 00:00:00 2001 From: desibooklover <desibooklover@gmail.com> Date: Wed, 22 Oct 2025 15:07:28 -0400 Subject: [PATCH 011/300] Translated using Weblate (Punjabi) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pa/ --- Emby.Server.Implementations/Localization/Core/pa.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pa.json b/Emby.Server.Implementations/Localization/Core/pa.json index 6062d97003..ced9204b46 100644 --- a/Emby.Server.Implementations/Localization/Core/pa.json +++ b/Emby.Server.Implementations/Localization/Core/pa.json @@ -134,6 +134,8 @@ "TaskCleanCollectionsAndPlaylistsDescription": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਵਿੱਚੋਂ ਉਹ ਆਈਟਮ ਹਟਾਉਂਦਾ ਹੈ ਜੋ ਹੁਣ ਮੌਜੂਦ ਨਹੀਂ ਹਨ।", "TaskCleanCollectionsAndPlaylists": "ਕਲੈਕਸ਼ਨਾਂ ਅਤੇ ਪਲੇਲਿਸਟਾਂ ਨੂੰ ਸਾਫ ਕਰੋ", "TaskAudioNormalization": "ਆਵਾਜ਼ ਸਧਾਰਣੀਕਰਨ", - "TaskRefreshTrickplayImagesDescription": "ਚਲ ਰਹੀ ਲਾਇਬ੍ਰੇਰੀਆਂ ਵਿੱਚ ਵੀਡੀਓਜ਼ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ।", - "TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।" + "TaskRefreshTrickplayImagesDescription": "ਵੀਡੀਓ ਲਈ ਟ੍ਰਿਕਪਲੇ ਪ੍ਰੀਵਿਊ ਬਣਾਉਂਦਾ ਹੈ (ਜੇ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਚੁਣਿਆ ਗਿਆ ਹੈ)।", + "TaskKeyframeExtractorDescription": "ਕੀ-ਫ੍ਰੇਮਜ਼ ਨੂੰ ਵੀਡੀਓ ਫਾਈਲਾਂ ਵਿੱਚੋਂ ਨਿਕਾਲਦਾ ਹੈ ਤਾਂ ਜੋ ਹੋਰ ਜ਼ਿਆਦਾ ਸਟਿਕ ਹੋਣ ਵਾਲੀਆਂ HLS ਪਲੇਲਿਸਟਾਂ ਬਣਾਈਆਂ ਜਾ ਸਕਣ। ਇਹ ਕੰਮ ਲੰਬੇ ਸਮੇਂ ਤੱਕ ਚੱਲ ਸਕਦਾ ਹੈ।", + "CleanupUserDataTaskDescription": "ਘੱਟੋ-ਘੱਟ 90 ਦਿਨਾਂ ਤੋਂ ਮੌਜੂਦ ਨਾ ਹੋਣ ਵਾਲੇ ਮੀਡੀਆ ਤੋਂ ਸਾਰੇ ਉਪਭੋਗਤਾ ਡੇਟਾ (ਵਾਚ ਸਟੇਟ, ਮਨਪਸੰਦ ਸਟੇਟਸ ਆਦਿ) ਨੂੰ ਸਾਫ਼ ਕਰਦਾ ਹੈ।", + "CleanupUserDataTask": "ਯੂਜ਼ਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨ ਦਾ ਕੰਮ" } From 0e3b6652b38dd8f7a2b42a1d4804f6feb1680440 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 23:59:06 +0000 Subject: [PATCH 012/300] Update github/codeql-action action to v4.31.0 --- .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 9a4c95e26c..53daf0991f 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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 From 788f090f27936db0f665a3fd9fce3b27549dd337 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 23:59:16 +0000 Subject: [PATCH 013/300] 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 a8104a917d..0651233386 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 7cca2af274..d37602690d 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: openapi-head path: openapi-head From c5affbbf71f9b8582d7ac9c88b294f50cc1a4fc7 Mon Sep 17 00:00:00 2001 From: HanHwanHo <ripper@me.com> Date: Fri, 24 Oct 2025 16:54:43 -0400 Subject: [PATCH 014/300] Translated using Weblate (Korean) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/ --- Emby.Server.Implementations/Localization/Core/ko.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index efc9f61ddf..3d1b1ed271 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -136,5 +136,7 @@ "TaskMoveTrickplayImages": "트릭플레이 이미지 위치 마이그레이션", "TaskMoveTrickplayImagesDescription": "추출된 트릭플레이 이미지를 라이브러리 설정에 따라 이동합니다.", "TaskDownloadMissingLyrics": "누락된 가사 다운로드", - "TaskDownloadMissingLyricsDescription": "가사 다운로드" + "TaskDownloadMissingLyricsDescription": "가사 다운로드", + "CleanupUserDataTask": "사용자 데이터 정리 작업", + "CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다." } From 6fd384729801f354fa256dd3d095e6f0cf7912ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:12:02 +0000 Subject: [PATCH 015/300] Update dependency z440.atl.core to 7.6.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d07384da0..12aab38288 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,7 +88,7 @@ <PackageVersion Include="System.Text.Json" Version="9.0.10" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.5.0" /> + <PackageVersion Include="z440.atl.core" Version="7.6.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 a6a8e29916891d04b34779f06bc3b3cf2f8e4e99 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:32:59 +0000 Subject: [PATCH 016/300] Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.18 --- .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 846835491a..b9fdd456f1 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@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17 + uses: danielpalme/ReportGenerator-GitHub-Action@f3c6b3f8a29686284ef7a7cf6dccb79a01d98444 # v5.4.18 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From cdc8325c7bb4d10a0f28a58135dcb59d512f2f09 Mon Sep 17 00:00:00 2001 From: Battseren Badral <bbattseren88@gmail.com> Date: Sat, 25 Oct 2025 13:20:45 -0400 Subject: [PATCH 017/300] Translated using Weblate (Mongolian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/ --- Emby.Server.Implementations/Localization/Core/mn.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index 240059a3bf..9202b65de8 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -54,7 +54,7 @@ "TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах", "External": "Гадны", "HeaderFavoriteArtists": "Дуртай уран бүтээлчид", - "HeaderFavoriteEpisodes": "Дуртай ангиуд", + "HeaderFavoriteEpisodes": "Хайртай Ангиуд", "HeaderFavoriteShows": "Дуртай нэвтрүүлэг", "HeaderFavoriteSongs": "Дуртай дуу", "AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}", @@ -71,7 +71,7 @@ "Forced": "Хүчээр", "HeaderAlbumArtists": "Цомгийн уран бүтээлчид", "HeaderFavoriteAlbums": "Дуртай цомгууд", - "HeaderLiveTV": "Шууд", + "HeaderLiveTV": "Шууд ТВ", "HeaderRecordingGroups": "Бичлэгийн бүлгүүд", "HearingImpaired": "Сонсголын бэрхшээлтэй", "HomeVideos": "Үндсэн дүрсүүд", 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/300] 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 d5fd5dfe6a04a829306da0e19efb08492ce189da Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Sun, 26 Oct 2025 21:50:38 -0400 Subject: [PATCH 019/300] Fix bump_version to handle spaced filename --- bump_version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bump_version b/bump_version index 6d08dc72fe..0516a1806d 100755 --- a/bump_version +++ b/bump_version @@ -58,7 +58,7 @@ for subproject in ${jellyfin_subprojects[@]}; do done # Set the version in the GitHub issue template file -sed -i "s|${old_version}|${new_version_sed}|g" ${issue_template_file} +sed -i "s|${old_version}|${new_version_sed}|g" "${issue_template_file}" # Stage the changed files for commit git add . From c274336563fa2f9c7e7b7df0126e815c812e528e Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Sun, 26 Oct 2025 21:52:03 -0400 Subject: [PATCH 020/300] Bump version to 10.12.0 (for real this time) --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 20b32f3a62..b84c961165 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Naming</PackageId> - <VersionPrefix>10.11.0</VersionPrefix> + <VersionPrefix>10.12.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 45374c22f7..fd852ece93 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Data</PackageId> - <VersionPrefix>10.11.0</VersionPrefix> + <VersionPrefix>10.12.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index de6be4707e..9af13b0a72 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Common</PackageId> - <VersionPrefix>10.11.0</VersionPrefix> + <VersionPrefix>10.12.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 3353ad63f1..b5d14e94b1 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Controller</PackageId> - <VersionPrefix>10.11.0</VersionPrefix> + <VersionPrefix>10.12.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index e9dab6bc8a..ef025d02dc 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Model</PackageId> - <VersionPrefix>10.11.0</VersionPrefix> + <VersionPrefix>10.12.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> diff --git a/SharedVersion.cs b/SharedVersion.cs index d26eb31aec..3b394d28b2 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.0")] -[assembly: AssemblyFileVersion("10.11.0")] +[assembly: AssemblyVersion("10.12.0")] +[assembly: AssemblyFileVersion("10.12.0")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 1613d83bc3..f52fd014da 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Extensions</PackageId> - <VersionPrefix>10.11.0</VersionPrefix> + <VersionPrefix>10.12.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> From 285e7c6c4f57479804ce46c84cbde874c7b31108 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Sun, 26 Oct 2025 22:07:46 -0400 Subject: [PATCH 021/300] Update CODEOWNERS to capture bump_version --- .github/CODEOWNERS | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e1900d5833..16d20303f5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,9 @@ -# Joshua must review all changes to deployment and build.sh -.ci/* @joshuaboniface -deployment/* @joshuaboniface -build.sh @joshuaboniface +# Joshua must review all changes to bump_version and any files it touches +bump_version @joshuaboniface +.github/ @joshuaboniface +MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface +Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface +MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface +MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface +Emby.Naming/Emby.Naming.csproj @joshuaboniface +src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface From c31ea251c47cdff9c663b33cb940411b5a863e9c Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" <joshua@boniface.me> Date: Sun, 26 Oct 2025 22:17:33 -0400 Subject: [PATCH 022/300] Improve handling of .github dir --- .github/CODEOWNERS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 16d20303f5..ef81678ddd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,9 +1,11 @@ # Joshua must review all changes to bump_version and any files it touches bump_version @joshuaboniface -.github/ @joshuaboniface +.github/ISSUE_TEMPLATE @joshuaboniface MediaBrowser.Common/MediaBrowser.Common.csproj @joshuaboniface Jellyfin.Data/Jellyfin.Data.csproj @joshuaboniface MediaBrowser.Controller/MediaBrowser.Controller.csproj @joshuaboniface MediaBrowser.Model/MediaBrowser.Model.csproj @joshuaboniface Emby.Naming/Emby.Naming.csproj @joshuaboniface src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @joshuaboniface +# Core must approve all changes within the repo config +.github/ @jellyfin/core From efd659412f96ed120866dab350705e1735c085ae Mon Sep 17 00:00:00 2001 From: kreaxv <kreaxv@gmail.com> Date: Mon, 27 Oct 2025 05:36:58 -0400 Subject: [PATCH 023/300] Translated using Weblate (Mongolian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/ --- Emby.Server.Implementations/Localization/Core/mn.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index 9202b65de8..cf39df706d 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -3,7 +3,7 @@ "HeaderNextUp": "Дараа нь", "HeaderContinueWatching": "Үргэлжлүүлэн үзэх", "Songs": "Дуунууд", - "Playlists": "Playlist-ууд", + "Playlists": "Тоглуулах жагсаалтууд", "Movies": "Кинонууд", "Latest": "Сүүлийн үеийн", "Genres": "Төрлүүд", @@ -54,7 +54,7 @@ "TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах", "External": "Гадны", "HeaderFavoriteArtists": "Дуртай уран бүтээлчид", - "HeaderFavoriteEpisodes": "Хайртай Ангиуд", + "HeaderFavoriteEpisodes": "Дуртай ангиуд", "HeaderFavoriteShows": "Дуртай нэвтрүүлэг", "HeaderFavoriteSongs": "Дуртай дуу", "AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}", @@ -111,7 +111,7 @@ "Shows": "Шоу", "Sync": "Дахин", "System": "Систем", - "TvShows": "ТВ нэвтрүүлгүүд", + "TvShows": "Цуварлууд", "Undefined": "Танисангүй", "User": "Хэрэглэгч", "UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ", 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 024/300] 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 9e53f46ad26b9ac815f12cdefa2ebfd41316d449 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:04 -0400 Subject: [PATCH 025/300] Backport pull request #15032 from jellyfin/release-10.11.z Skip invalid keyframe cache data Original-merge: f4a53209f4a3dde62acb7027d9b48eaa08d9ef8c Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Migrations/Routines/MigrateKeyframeData.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index c199ee4d6b..612da05214 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -135,14 +135,21 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine return Path.Join(keyframeCachePath, prefix, filename); } - private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult) + private bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult) { if (File.Exists(cachePath)) { - var bytes = File.ReadAllBytes(cachePath); - cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions); + try + { + var bytes = File.ReadAllBytes(cachePath); + cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions); - return cachedResult is not null; + return cachedResult is not null; + } + catch (JsonException jsonException) + { + _logger.LogWarning(jsonException, "Failed to read {Path}", cachePath); + } } cachedResult = null; From cee16d47cb515f268fe38b27f387a1e1a044b2a7 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:06 -0400 Subject: [PATCH 026/300] Backport pull request #15054 from jellyfin/release-10.11.z Speed-up trickplay migration Original-merge: ca830d5be7c7a173f91ae7521d43cb47484718f1 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index c9630b8945..1510e537d3 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -152,6 +152,10 @@ namespace Emby.Server.Implementations.IO /// <inheritdoc /> public void MoveDirectory(string source, string destination) { + // Make sure parent directory of target exists + var parent = Directory.GetParent(destination); + parent?.Create(); + try { Directory.Move(source, destination); From 9f8fb6d5886a645aceeb9782b93e0f8d550e6565 Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:07 -0400 Subject: [PATCH 027/300] Backport pull request #15055 from jellyfin/release-10.11.z Log the message more clear when network manager is not ready Original-merge: a245605152c2871c413102a32c30230e6c603eae Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 + Jellyfin.Server/ServerSetupApp/index.mstemplate.html | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 72626e8532..00d9fcc025 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -250,6 +250,7 @@ public sealed class SetupServer : IDisposable { "isInReportingMode", _isUnhealthy }, { "retryValue", retryAfterValue }, { "logs", startupLogEntries }, + { "networkManagerReady", networkManager is not null }, { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) } }, new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions)) diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html index 523f38d74a..9ec6efa2b9 100644 --- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html +++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html @@ -213,7 +213,12 @@ </ol> </div> {{#ELSE}} + {{#IF networkManagerReady}} <p>Please visit this page from your local network to view detailed startup logs.</p> + {{#ELSE}} + <p>Initializing network settings. Please wait.</p> + {{/ELSE}} + {{/IF}} {{/ELSE}} {{/IF}} </div> From 348b2992d7027138a57d35362100b1b93d68cc9a Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Mon, 27 Oct 2025 15:43:08 -0400 Subject: [PATCH 028/300] Backport pull request #15072 from jellyfin/release-10.11.z Reject stream copy of HDR10+ video if the client does not support HDR10 Original-merge: a725220c219d98ea69bc01d2664e68d58d0230f0 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 c81e639a22..a1d8915353 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2390,8 +2390,8 @@ namespace MediaBrowser.Controller.MediaEncoding || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR) || (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus))) { - // If the video stream is in a static HDR format, don't allow copy if the client does not support HDR10 or HLG. - if (videoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG) + // If the video stream is in HDR10+ or a static HDR format, don't allow copy if the client does not support HDR10 or HLG. + if (videoStream.VideoRangeType is VideoRangeType.HDR10Plus or VideoRangeType.HDR10 or VideoRangeType.HLG) { return false; } From 5deb69b23f3841f9c0f440dd2e5c1131974a43b5 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:09 -0400 Subject: [PATCH 029/300] Backport pull request #15083 from jellyfin/release-10.11.z Fix LiveTV images not saving to database Original-merge: d738386fe2032be80d4b5bbfd2839b0cb2f397dc Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index eb88eac00a..883c4542c3 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -614,6 +614,13 @@ public sealed class BaseItemRepository else { context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + + if (entity.Images is { Count: > 0 }) + { + context.BaseItemImageInfos.AddRange(entity.Images); + } + context.BaseItems.Attach(entity).State = EntityState.Modified; } } From adfa5200573bcba070b6872945fe5ade8850b7f6 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:10 -0400 Subject: [PATCH 030/300] Backport pull request #15087 from jellyfin/release-10.11.z Optimize WhereReferencedItemMultipleTypes filtering Original-merge: a5bc4524d8a882efa12a17b6251894322745be78 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 2 +- .../JellyfinQueryHelperExtensions.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 883c4542c3..20a40c80de 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2053,7 +2053,7 @@ public sealed class BaseItemRepository if (filter.ExcludeArtistIds.Length > 0) { - baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true); + baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true); } if (filter.GenreIds.Count > 0) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs index 8cb483f491..f386e882e2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs @@ -70,13 +70,14 @@ public static class JellyfinQueryHelperExtensions bool invert = false) { var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id); + var typeFilter = OneOrManyExpressionBuilder<ItemValue, ItemValueType>(itemValueTypes, iv => iv.Type); return baseQuery.Where(item => context.ItemValues + .Where(typeFilter) .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (itemVal, map) => new { itemVal, map }) .Any(val => - itemValueTypes.Contains(val.itemVal.Type) - && context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue) + context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.itemVal.CleanValue) && val.map.ItemId == item.Id) == EF.Constant(!invert)); } From 54d28d984282e0256cdb302c37f2947c4db15578 Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Mon, 27 Oct 2025 15:43:11 -0400 Subject: [PATCH 031/300] Backport pull request #15098 from jellyfin/release-10.11.z Lower required tmp dir size to 512MiB Original-merge: 0a6e8146be0ca207a9d4b30fa4eaa117a27786cd Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../StorageHelpers/StorageHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index b2f54be7e2..570d6cb9b7 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -14,7 +14,7 @@ public static class StorageHelper { private const long TwoGigabyte = 2_147_483_647L; private const long FiveHundredAndTwelveMegaByte = 536_870_911L; - private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; + private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; /// <summary> /// Tests the available storage capacity on the jellyfin paths with estimated minimum values. @@ -27,7 +27,7 @@ public static class StorageHelper TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte); TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte); TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte); - TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte); + TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte); } /// <summary> @@ -77,7 +77,7 @@ public static class StorageHelper var drive = new DriveInfo(path); if (threshold != -1 && drive.AvailableFreeSpace < threshold) { - throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}."); + throw new InvalidOperationException($"The path `{path}` has insufficient free space. Available: {HumanizeStorageSize(drive.AvailableFreeSpace)}, Required: {HumanizeStorageSize(threshold)}."); } logger.LogInformation( From 3c4dc16003bedb5201b0fa68652421c625523a15 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:12 -0400 Subject: [PATCH 032/300] Backport pull request #15102 from jellyfin/release-10.11.z Make season paths case-insensitive Original-merge: 305b0fdca323833653dd26c642b2c465890143dc Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Naming/TV/SeasonPathParser.cs | 6 +++--- .../TV/SeasonPathParserTests.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 98ee1e4b8f..90aae2d485 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -10,10 +10,10 @@ namespace Emby.Naming.TV /// </summary> public static partial class SeasonPathParser { - [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")] + [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPre(); - [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")] + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPost(); /// <summary> @@ -86,7 +86,7 @@ namespace Emby.Naming.TV } } - if (filename.StartsWith('s')) + if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's')) { var testFilename = filename.AsSpan()[1..]; diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 4c8ba58d04..7671166ff4 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -7,23 +7,38 @@ public class SeasonPathParserTests { [Theory] [InlineData("/Drive/Season 1", "/Drive", 1, true)] + [InlineData("/Drive/SEASON 1", "/Drive", 1, true)] [InlineData("/Drive/Staffel 1", "/Drive", 1, true)] + [InlineData("/Drive/STAFFEL 1", "/Drive", 1, true)] [InlineData("/Drive/Stagione 1", "/Drive", 1, true)] + [InlineData("/Drive/STAGIONE 1", "/Drive", 1, true)] [InlineData("/Drive/sæson 1", "/Drive", 1, true)] + [InlineData("/Drive/SÆSON 1", "/Drive", 1, true)] [InlineData("/Drive/Temporada 1", "/Drive", 1, true)] + [InlineData("/Drive/TEMPORADA 1", "/Drive", 1, true)] [InlineData("/Drive/series 1", "/Drive", 1, true)] + [InlineData("/Drive/SERIES 1", "/Drive", 1, true)] [InlineData("/Drive/Kausi 1", "/Drive", 1, true)] + [InlineData("/Drive/KAUSI 1", "/Drive", 1, true)] [InlineData("/Drive/Säsong 1", "/Drive", 1, true)] + [InlineData("/Drive/SÄSONG 1", "/Drive", 1, true)] [InlineData("/Drive/Seizoen 1", "/Drive", 1, true)] + [InlineData("/Drive/SEIZOEN 1", "/Drive", 1, true)] [InlineData("/Drive/Seasong 1", "/Drive", 1, true)] + [InlineData("/Drive/SEASONG 1", "/Drive", 1, true)] [InlineData("/Drive/Sezon 1", "/Drive", 1, true)] + [InlineData("/Drive/SEZON 1", "/Drive", 1, true)] [InlineData("/Drive/sezona 1", "/Drive", 1, true)] + [InlineData("/Drive/SEZONA 1", "/Drive", 1, true)] [InlineData("/Drive/sezóna 1", "/Drive", 1, true)] + [InlineData("/Drive/SEZÓNA 1", "/Drive", 1, true)] [InlineData("/Drive/Sezonul 1", "/Drive", 1, true)] + [InlineData("/Drive/SEZONUL 1", "/Drive", 1, true)] [InlineData("/Drive/시즌 1", "/Drive", 1, true)] [InlineData("/Drive/シーズン 1", "/Drive", 1, true)] [InlineData("/Drive/сезон 1", "/Drive", 1, true)] [InlineData("/Drive/Сезон 1", "/Drive", 1, true)] + [InlineData("/Drive/СЕЗОН 1", "/Drive", 1, true)] [InlineData("/Drive/Season 10", "/Drive", 10, true)] [InlineData("/Drive/Season 100", "/Drive", 100, true)] [InlineData("/Drive/s1", "/Drive", 1, true)] @@ -46,8 +61,11 @@ public class SeasonPathParserTests [InlineData("/Drive/s06e05", "/Drive", null, false)] [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)] [InlineData("/Drive/extras", "/Drive", 0, true)] + [InlineData("/Drive/EXTRAS", "/Drive", 0, true)] [InlineData("/Drive/specials", "/Drive", 0, true)] + [InlineData("/Drive/SPECIALS", "/Drive", 0, true)] [InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)] + [InlineData("/Drive/Episode 1 SEASON 2", "/Drive", null, false)] public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, parentPath, true, true); From 14e819458150c237cf613823d41d16c051f61277 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:13 -0400 Subject: [PATCH 033/300] Backport pull request #15112 from jellyfin/release-10.11.z Skip extracted files in migration if bad timestamp or no access Original-merge: 7a1c1cd3421a39c09b969a581955fda4f3f81ec5 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Migrations/Routines/MoveExtractedFiles.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index 8b394dd7aa..fbf9c16377 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -224,6 +224,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine return null; } + catch (UnauthorizedAccessException e) + { + _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message); + + return null; + } + catch (ArgumentOutOfRangeException e) + { + _logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message); + + return null; + } filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); } @@ -263,6 +275,18 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine { date = File.GetLastWriteTimeUtc(path); } + catch (ArgumentOutOfRangeException e) + { + _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message); + + return null; + } + catch (UnauthorizedAccessException e) + { + _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message); + + return null; + } catch (IOException e) { _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message); From 3f2ebc41794f272cde6e5785f444b3059bca9544 Mon Sep 17 00:00:00 2001 From: ivanjx <ivanjx@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:14 -0400 Subject: [PATCH 034/300] Backport pull request #15113 from jellyfin/release-10.11.z Add season number fallback for OMDB and TMDB plugins Original-merge: 618ec4543e48fc670e655eda39c2e3869be86c7b Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs | 2 ++ MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index ad9edb031c..82c6e3011a 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -138,6 +138,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb } var item = itemResult.Item; + item.IndexNumber = episodeNumber; + item.ParentIndexNumber = seasonNumber; var seasonResult = await GetSeasonRootObject(seriesImdbId, seasonNumber, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index 0953dde1ce..e30c555cb4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -177,8 +177,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var item = new Episode { - IndexNumber = info.IndexNumber, - ParentIndexNumber = info.ParentIndexNumber, + IndexNumber = episodeNumber, + ParentIndexNumber = seasonNumber, IndexNumberEnd = info.IndexNumberEnd, Name = episodeResult.Name, PremiereDate = episodeResult.AirDate, From 0931d6e4de9e120af8a4b2d3b26a1587d22d445b Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:15 -0400 Subject: [PATCH 035/300] Backport pull request #15126 from jellyfin/release-10.11.z Fix Has(Imdb/Tmdb/Tvdb)Id checks Original-merge: 14b3085ff1c83d0dbf691d0ba38c00d035d46bb2 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 20a40c80de..8319bfd944 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2360,17 +2360,23 @@ public sealed class BaseItemRepository if (filter.HasImdbId.HasValue) { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb")); + baseQuery = filter.HasImdbId.Value + ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower())) + : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower())); } if (filter.HasTmdbId.HasValue) { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb")); + baseQuery = filter.HasTmdbId.Value + ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower())) + : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower())); } if (filter.HasTvdbId.HasValue) { - baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb")); + baseQuery = filter.HasTvdbId.Value + ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower())) + : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower())); } var queryTopParentIds = filter.TopParentIds; From 4c1c160990f82583742c129048ff3db375f01017 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:17 -0400 Subject: [PATCH 036/300] Backport pull request #15133 from jellyfin/release-10.11.z Play selected song first with instant mix Original-merge: 1520a697ad43f3f023608f8012cce1f52926b5fe Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Library/MusicManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index e0c8ae371b..e19ad3ef6e 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -28,7 +28,9 @@ namespace Emby.Server.Implementations.Library public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions) { - return GetInstantMixFromGenres(item.Genres, user, dtoOptions); + var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions); + + return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))]; } /// <inheritdoc /> From dbb015441fdbdeb0d29714ffd3f5b168e7081021 Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Mon, 27 Oct 2025 15:43:18 -0400 Subject: [PATCH 037/300] Backport pull request #15144 from jellyfin/release-10.11.z Fix videos with cropping metadata are probed as anamorphic Original-merge: 175ee12bbcad1394d9cf9696d8408a9dd5190b8e Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Probing/ProbeResultNormalizer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 00a9ae797d..eb312029a1 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -930,6 +930,15 @@ namespace MediaBrowser.MediaEncoding.Probing { stream.Rotation = data.Rotation; } + + // Parse video frame cropping metadata from side_data + // TODO: save them and make HW filters to apply them in HWA pipelines + else if (string.Equals(data.SideDataType, "Frame Cropping", StringComparison.OrdinalIgnoreCase)) + { + // Streams containing artificially added frame cropping + // metadata should not be marked as anamorphic. + stream.IsAnamorphic = false; + } } } From 232c0399e27b7d922362f543ec993e1208faef69 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 27 Oct 2025 15:43:19 -0400 Subject: [PATCH 038/300] Backport pull request #15164 from jellyfin/release-10.11.z Fix XmlOutputFormatter Original-merge: 2b94bb54aa1669abc2e0975f1a089389bcc6052a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Api/Formatters/XmlOutputFormatter.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs index 8dbb91d0aa..46256c09d7 100644 --- a/Jellyfin.Api/Formatters/XmlOutputFormatter.cs +++ b/Jellyfin.Api/Formatters/XmlOutputFormatter.cs @@ -1,4 +1,8 @@ +using System; using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Formatters; namespace Jellyfin.Api.Formatters; @@ -6,7 +10,7 @@ namespace Jellyfin.Api.Formatters; /// <summary> /// Xml output formatter. /// </summary> -public sealed class XmlOutputFormatter : StringOutputFormatter +public sealed class XmlOutputFormatter : TextOutputFormatter { /// <summary> /// Initializes a new instance of the <see cref="XmlOutputFormatter"/> class. @@ -15,5 +19,24 @@ public sealed class XmlOutputFormatter : StringOutputFormatter { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeNames.Text.Xml); + + SupportedEncodings.Add(Encoding.UTF8); + SupportedEncodings.Add(Encoding.Unicode); + } + + /// <inheritdoc /> + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(selectedEncoding); + + var valueAsString = context.Object?.ToString(); + if (string.IsNullOrEmpty(valueAsString)) + { + return; + } + + var response = context.HttpContext.Response; + await response.WriteAsync(valueAsString, selectedEncoding).ConfigureAwait(false); } } From ed6cb30762dbeb018209e19a8896059e4f906c1b Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 27 Oct 2025 15:43:20 -0400 Subject: [PATCH 039/300] Backport pull request #15170 from jellyfin/release-10.11.z Clean up BackupService Original-merge: ac3fa3c376a47c099e14d4b940832c39e2249aee Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../FullSystemBackup/BackupService.cs | 236 ++++++++++-------- 1 file changed, 131 insertions(+), 105 deletions(-) diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index e5c3cef3d3..e39a2b42f6 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -199,7 +199,7 @@ public class BackupService : IBackupService var zipEntry = zipArchive.GetEntry(NormalizePathSeparator(Path.Combine("Database", $"{entityType.Type.Name}.json"))); if (zipEntry is null) { - _logger.LogInformation("No backup of expected table {Table} is present in backup. Continue anyway.", entityType.Type.Name); + _logger.LogInformation("No backup of expected table {Table} is present in backup, continuing anyway", entityType.Type.Name); continue; } @@ -223,7 +223,7 @@ public class BackupService : IBackupService } catch (Exception ex) { - _logger.LogError(ex, "Could not store entity {Entity} continue anyway.", item); + _logger.LogError(ex, "Could not store entity {Entity}, continuing anyway", item); } } @@ -233,11 +233,11 @@ public class BackupService : IBackupService _logger.LogInformation("Try restore Database"); await dbContext.SaveChangesAsync().ConfigureAwait(false); - _logger.LogInformation("Restored database."); + _logger.LogInformation("Restored database"); } } - _logger.LogInformation("Restored Jellyfin system from {Date}.", manifest.DateCreated); + _logger.LogInformation("Restored Jellyfin system from {Date}", manifest.DateCreated); } } @@ -263,6 +263,8 @@ public class BackupService : IBackupService Options = Map(backupOptions) }; + _logger.LogInformation("Running database optimization before backup"); + await _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false); var backupFolder = Path.Combine(_applicationPaths.BackupPath); @@ -281,130 +283,154 @@ public class BackupService : IBackupService } var backupPath = Path.Combine(backupFolder, $"jellyfin-backup-{manifest.DateCreated.ToLocalTime():yyyyMMddHHmmss}.zip"); - _logger.LogInformation("Attempt to create a new backup at {BackupPath}", backupPath); - var fileStream = File.OpenWrite(backupPath); - await using (fileStream.ConfigureAwait(false)) - using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) + + try { - _logger.LogInformation("Start backup process."); - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) + _logger.LogInformation("Attempting to create a new backup at {BackupPath}", backupPath); + var fileStream = File.OpenWrite(backupPath); + await using (fileStream.ConfigureAwait(false)) + using (var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) { - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - static IAsyncEnumerable<object> GetValues(IQueryable dbSet) + _logger.LogInformation("Starting backup process"); + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!; - var enumerable = method.Invoke(dbSet, null)!; - return (IAsyncEnumerable<object>)enumerable; - } + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - // include the migration history as well - var historyRepository = dbContext.GetService<IHistoryRepository>(); - var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false); - - ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [ - .. typeof(JellyfinDbContext) - .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) - .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) - .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))), - (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable()) - ]; - manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray(); - var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false); - - await using (transaction.ConfigureAwait(false)) - { - _logger.LogInformation("Begin Database backup"); - - foreach (var entityType in entityTypes) + static IAsyncEnumerable<object> GetValues(IQueryable dbSet) { - _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(); - await using (zipEntryStream.ConfigureAwait(false)) + var method = dbSet.GetType().GetMethod(nameof(DbSet<object>.AsAsyncEnumerable))!; + var enumerable = method.Invoke(dbSet, null)!; + return (IAsyncEnumerable<object>)enumerable; + } + + // include the migration history as well + var historyRepository = dbContext.GetService<IHistoryRepository>(); + var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false); + + ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = + [ + .. typeof(JellyfinDbContext) + .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable))) + .Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!)))), + (Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable()) + ]; + manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray(); + var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false); + + await using (transaction.ConfigureAwait(false)) + { + _logger.LogInformation("Begin Database backup"); + + foreach (var entityType in entityTypes) { - var jsonSerializer = new Utf8JsonWriter(zipEntryStream); - await using (jsonSerializer.ConfigureAwait(false)) + _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(); + await using (zipEntryStream.ConfigureAwait(false)) { - jsonSerializer.WriteStartArray(); - - var set = entityType.ValueFactory().ConfigureAwait(false); - await foreach (var item in set.ConfigureAwait(false)) + var jsonSerializer = new Utf8JsonWriter(zipEntryStream); + await using (jsonSerializer.ConfigureAwait(false)) { - entities++; - try + jsonSerializer.WriteStartArray(); + + var set = entityType.ValueFactory().ConfigureAwait(false); + await foreach (var item in set.ConfigureAwait(false)) { - JsonSerializer.SerializeToDocument(item, _serializerSettings).WriteTo(jsonSerializer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not load entity {Entity}", item); - throw; + entities++; + try + { + using var document = JsonSerializer.SerializeToDocument(item, _serializerSettings); + document.WriteTo(jsonSerializer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not load entity {Entity}", item); + throw; + } } + + jsonSerializer.WriteEndArray(); } - - jsonSerializer.WriteEndArray(); } - } - _logger.LogInformation("backup of entity {Table} with {Number} created", entityType.Type.Name, entities); + _logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities); + } } } - } - _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath); - 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)))); - } - - void CopyDirectory(string source, string target, string filter = "*") - { - if (!Directory.Exists(source)) + _logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath); + foreach (var item in Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.xml", SearchOption.TopDirectoryOnly) + .Union(Directory.EnumerateFiles(_applicationPaths.ConfigurationDirectoryPath, "*.json", SearchOption.TopDirectoryOnly))) { - return; + zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine("Config", Path.GetFileName(item)))); } - _logger.LogInformation("Backup of folder {Table}", source); - - foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories)) + void CopyDirectory(string source, string target, string filter = "*") { - zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); + if (!Directory.Exists(source)) + { + return; + } + + _logger.LogInformation("Backup of folder {Table}", source); + + foreach (var item in Directory.EnumerateFiles(source, filter, SearchOption.AllDirectories)) + { + zipArchive.CreateEntryFromFile(item, NormalizePathSeparator(Path.Combine(target, Path.GetRelativePath(source, item)))); + } + } + + CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users")); + CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks")); + CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root"); + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections")); + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists")); + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks")); + if (backupOptions.Subtitles) + { + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles")); + } + + if (backupOptions.Trickplay) + { + CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay")); + } + + if (backupOptions.Metadata) + { + CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); + } + + var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open(); + await using (manifestStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false); } } - CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "users"), Path.Combine("Config", "users")); - CopyDirectory(Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"), Path.Combine("Config", "ScheduledTasks")); - CopyDirectory(Path.Combine(_applicationPaths.RootFolderPath), "Root"); - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "collections"), Path.Combine("Data", "collections")); - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "playlists"), Path.Combine("Data", "playlists")); - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"), Path.Combine("Data", "ScheduledTasks")); - if (backupOptions.Subtitles) - { - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "subtitles"), Path.Combine("Data", "subtitles")); - } - - if (backupOptions.Trickplay) - { - CopyDirectory(Path.Combine(_applicationPaths.DataPath, "trickplay"), Path.Combine("Data", "trickplay")); - } - - if (backupOptions.Metadata) - { - CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); - } - - var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open(); - await using (manifestStream.ConfigureAwait(false)) - { - await JsonSerializer.SerializeAsync(manifestStream, manifest).ConfigureAwait(false); - } + _logger.LogInformation("Backup created"); + return Map(manifest, backupPath); } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create backup, removing {BackupPath}", backupPath); + try + { + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + } + } + catch (Exception innerEx) + { + _logger.LogWarning(innerEx, "Unable to remove failed backup"); + } - _logger.LogInformation("Backup created"); - return Map(manifest, backupPath); + throw; + } } /// <inheritdoc/> @@ -422,7 +448,7 @@ public class BackupService : IBackupService } catch (Exception ex) { - _logger.LogError(ex, "Tried to load archive from {Path} but failed.", archivePath); + _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", archivePath); return null; } @@ -459,7 +485,7 @@ public class BackupService : IBackupService } catch (Exception ex) { - _logger.LogError(ex, "Could not load {BackupArchive} path.", item); + _logger.LogWarning(ex, "Tried to load manifest from archive {Path} but failed", item); } } From 6514196e8d975f0ba3e904c4ea7638d73ea603a4 Mon Sep 17 00:00:00 2001 From: CeruleanRed <64965209+CeruleanRed@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:21 -0400 Subject: [PATCH 040/300] Backport pull request #15176 from jellyfin/release-10.11.z Only save chapters that are within the runtime of the video file Original-merge: 442af96ed9c7b9cfadf46e85e8119ac0476408e0 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Server.Implementations/Chapters/ChapterManager.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs index fea05931d7..d09ed30ae3 100644 --- a/Emby.Server.Implementations/Chapters/ChapterManager.cs +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -223,7 +223,7 @@ public class ChapterManager : IChapterManager if (saveChapters && changesMade) { - _chapterRepository.SaveChapters(video.Id, chapters); + SaveChapters(video, chapters); } DeleteDeadImages(currentImages, chapters); @@ -234,7 +234,9 @@ public class ChapterManager : IChapterManager /// <inheritdoc /> public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters) { - _chapterRepository.SaveChapters(video.Id, chapters); + // Remove any chapters that are outside of the runtime of the video + var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList(); + _chapterRepository.SaveChapters(video.Id, validChapters); } /// <inheritdoc /> From 511b5d9c537e45dcbcb4bc464ed5910ea5546399 Mon Sep 17 00:00:00 2001 From: gnattu <gnattu@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:22 -0400 Subject: [PATCH 041/300] Backport pull request #15177 from jellyfin/release-10.11.z Make priority class setting more robust Original-merge: 70c32a26fa9f16db513a92cdd2dcafa7ee15a80d Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 10 +++++++++- .../FfProbe/FfProbeKeyframeExtractor.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 8350d1613b..b7fef842b3 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1122,7 +1122,15 @@ namespace MediaBrowser.MediaEncoding.Encoder private void StartProcess(ProcessWrapper process) { process.Process.Start(); - process.Process.PriorityClass = ProcessPriorityClass.BelowNormal; + + try + { + process.Process.PriorityClass = ProcessPriorityClass.BelowNormal; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName); + } lock (_runningProcessesLock) { diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs index a0dafb8f19..cbe97a8210 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs @@ -42,7 +42,15 @@ public static class FfProbeKeyframeExtractor try { process.Start(); - process.PriorityClass = ProcessPriorityClass.BelowNormal; + try + { + process.PriorityClass = ProcessPriorityClass.BelowNormal; + } + catch + { + // We do not care if process priority setting fails + // Ideally log a warning but this does not have a logger available + } return ParseStream(process.StandardOutput); } From 08fd175f5aa3a921d44934a11ccb8bcbd3956120 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:23 -0400 Subject: [PATCH 042/300] Backport pull request #15187 from jellyfin/release-10.11.z Fix pagination and sorting for folders Original-merge: 7d1824ea27093322d5e8316ee38f375129f40386 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/Folder.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e9a3836902..03ee447088 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -715,9 +715,18 @@ namespace MediaBrowser.Controller.Entities } else { - items = GetRecursiveChildren(user, query, out totalCount); + // Save pagination params before clearing them to prevent pagination from happening + // before sorting. PostFilterAndSort will apply pagination after sorting. + var limit = query.Limit; + var startIndex = query.StartIndex; query.Limit = null; - query.StartIndex = null; // override these here as they have already been applied + query.StartIndex = null; + + items = GetRecursiveChildren(user, query, out totalCount); + + // Restore pagination params so PostFilterAndSort can apply them after sorting + query.Limit = limit; + query.StartIndex = startIndex; } var result = PostFilterAndSort(items, query); @@ -980,20 +989,16 @@ namespace MediaBrowser.Controller.Entities else { // need to pass this param to the children. + // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort var childQuery = new InternalItemsQuery { DisplayAlbumFolders = query.DisplayAlbumFolders, - Limit = query.Limit, - StartIndex = query.StartIndex, NameStartsWith = query.NameStartsWith, NameStartsWithOrGreater = query.NameStartsWithOrGreater, NameLessThan = query.NameLessThan }; items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); - - query.Limit = null; - query.StartIndex = null; } var result = PostFilterAndSort(items, query); From 570b8b2eb979c3dbcf996a6fa9b4c29d3c27f230 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 27 Oct 2025 15:43:24 -0400 Subject: [PATCH 043/300] Backport pull request #15194 from jellyfin/release-10.11.z Initialize transcode marker during startup Original-merge: 81b8b0ca4a1e33ffd2aa2ddde1fa0561ee6a6c4a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server/Program.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index dc7fa5eb36..93f71fdc69 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -184,6 +184,12 @@ namespace Jellyfin.Server .AddSingleton<IServiceCollection>(e)) .Build(); + /* + * Initialize the transcode path marker so we avoid starting Jellyfin in a broken state. + * This should really be a part of IApplicationPaths but this path is configured differently. + */ + _ = appHost.ConfigurationManager.GetTranscodePath(); + // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = _jellyfinHost.Services; PrepareDatabaseProvider(appHost.ServiceProvider); From 0f21222a0c873b9e3e258ec6c3ed32278b41614f Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 27 Oct 2025 15:43:25 -0400 Subject: [PATCH 044/300] Backport pull request #15196 from jellyfin/release-10.11.z Skip directory entry when restoring from backup Original-merge: 0e4031ae52b2ca3a19e22bfc6ab9c9af88944bd8 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../FullSystemBackup/BackupService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index e39a2b42f6..70483c36cc 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -128,7 +128,8 @@ public class BackupService : IBackupService var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName))); if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal) - || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal)) + || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal) + || Path.EndsInDirectorySeparator(item.FullName)) { continue; } From 4dc826644d213ee47f4181009be3c928448b0879 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 27 Oct 2025 15:43:26 -0400 Subject: [PATCH 045/300] Backport pull request #15197 from jellyfin/release-10.11.z Filter plugins by id instead of name Original-merge: 5691eee4f16402dfe528787666eef13678faaba0 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Updates/InstallationManager.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 678475b31f..5ff4001601 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -223,15 +223,14 @@ namespace Emby.Server.Implementations.Updates Guid id = default, Version? specificVersion = null) { - if (name is not null) - { - availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - } - if (!id.IsEmpty()) { availablePackages = availablePackages.Where(x => x.Id.Equals(id)); } + else if (name is not null) + { + availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + } if (specificVersion is not null) { From 423c2654c087e9c23fafe6766ccc7168b6b2dd3a Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:27 -0400 Subject: [PATCH 046/300] Backport pull request #15209 from jellyfin/release-10.11.z Improve symlink handling Original-merge: e5656af1f2e740c6e4f78f613d47d37567940ed8 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../IO/ManagedFileSystem.cs | 47 +++--- .../Library/DotIgnoreIgnoreRule.cs | 9 +- ...linkFollowingPhysicalFileResultExecutor.cs | 151 ------------------ Jellyfin.Server/Startup.cs | 5 - 4 files changed, 27 insertions(+), 185 deletions(-) delete mode 100644 Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 1510e537d3..97e89ca3d9 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -252,47 +252,40 @@ namespace Emby.Server.Implementations.IO { result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory; - // if (!result.IsDirectory) - // { - // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; - // } - if (info is FileInfo fileInfo) { - result.Length = fileInfo.Length; - - // Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes! - if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) + result.CreationTimeUtc = GetCreationTimeUtc(info); + result.LastWriteTimeUtc = GetLastWriteTimeUtc(info); + if (fileInfo.LinkTarget is not null) { try { - using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true); + if (targetFileInfo is not null) { - result.Length = RandomAccess.GetLength(fileHandle); + result.Exists = targetFileInfo.Exists; + if (result.Exists) + { + result.Length = targetFileInfo.Length; + result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo); + result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo); + } + } + else + { + result.Exists = false; } - } - catch (FileNotFoundException ex) - { - // Dangling symlinks cannot be detected before opening the file unfortunately... - _logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName); - result.Exists = false; } catch (UnauthorizedAccessException ex) { _logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName); } - catch (IOException ex) - { - // IOException generally means the file is not accessible due to filesystem issues - // Catch this exception and mark the file as not exist to ignore it - _logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName); - result.Exists = false; - } + } + else + { + result.Length = fileInfo.Length; } } - - result.CreationTimeUtc = GetCreationTimeUtc(info); - result.LastWriteTimeUtc = GetLastWriteTimeUtc(info); } else { diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index bafe3ad436..959acd4751 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule } // Fast path in case the ignore files isn't a symlink and is empty - if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0 - && dirIgnoreFile.Length == 0) + if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0) { return true; } @@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule private static string GetFileContent(FileInfo dirIgnoreFile) { + dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile; + if (!dirIgnoreFile.Exists) + { + return string.Empty; + } + using (var reader = dirIgnoreFile.OpenText()) { return reader.ReadToEnd(); diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs deleted file mode 100644 index 910b5c4672..0000000000 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ /dev/null @@ -1,151 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) .NET Foundation and Contributors -// -// All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Jellyfin.Server.Infrastructure -{ - /// <inheritdoc /> - public class SymlinkFollowingPhysicalFileResultExecutor : PhysicalFileResultExecutor - { - /// <summary> - /// Initializes a new instance of the <see cref="SymlinkFollowingPhysicalFileResultExecutor"/> class. - /// </summary> - /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param> - public SymlinkFollowingPhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } - - /// <inheritdoc /> - protected override FileMetadata GetFileInfo(string path) - { - var fileInfo = new FileInfo(path); - var length = fileInfo.Length; - // This may or may not be fixed in .NET 6, but looks like it will not https://github.com/dotnet/aspnetcore/issues/34371 - if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) - { - using var fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - length = RandomAccess.GetLength(fileHandle); - } - - return new FileMetadata - { - Exists = fileInfo.Exists, - Length = length, - LastModified = fileInfo.LastWriteTimeUtc - }; - } - - /// <inheritdoc /> - protected override async Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength) - { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(result); - - if (range is not null && rangeLength == 0) - { - return; - } - - // It's a bit of wasted IO to perform this check again, but non-symlinks shouldn't use this code - if (!IsSymLink(result.FileName)) - { - await base.WriteFileAsync(context, result, range, rangeLength).ConfigureAwait(false); - return; - } - - var response = context.HttpContext.Response; - - if (range is not null) - { - await SendFileAsync( - result.FileName, - response, - offset: range.From ?? 0L, - count: rangeLength).ConfigureAwait(false); - return; - } - - await SendFileAsync( - result.FileName, - response, - offset: 0, - count: null).ConfigureAwait(false); - } - - private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default) - { - var fileInfo = GetFileInfo(filePath); - if (offset < 0 || offset > fileInfo.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); - } - - if (count.HasValue - && (count.Value < 0 || count.Value > fileInfo.Length - offset)) - { - throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); - } - - // Copied from SendFileFallback.SendFileAsync - const int BufferSize = 1024 * 16; - - var useRequestAborted = !cancellationToken.CanBeCanceled; - var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken; - - var fileStream = new FileStream( - filePath, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite, - bufferSize: BufferSize, - options: FileOptions.Asynchronous | FileOptions.SequentialScan); - await using (fileStream.ConfigureAwait(false)) - { - try - { - localCancel.ThrowIfCancellationRequested(); - fileStream.Seek(offset, SeekOrigin.Begin); - await StreamCopyOperation - .CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel) - .ConfigureAwait(true); - } - catch (OperationCanceledException) when (useRequestAborted) - { - } - } - } - - private static bool IsSymLink(string path) => (File.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint; - } -} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index aa8f6dd1cd..5032b2aec1 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -16,15 +16,12 @@ using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Implementations.Extensions; -using Jellyfin.Server.Infrastructure; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using MediaBrowser.XbmcMetadata; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -69,8 +66,6 @@ namespace Jellyfin.Server options.HttpsPort = _serverApplicationHost.HttpsPort; }); - // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 - services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration); services.AddJellyfinApiSwagger(); From a489942454218fc968baf7858ee00ee9141ebfb5 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Mon, 27 Oct 2025 15:43:29 -0400 Subject: [PATCH 047/300] Backport pull request #15212 from jellyfin/release-10.11.z Skip invalid database migration Original-merge: 2966d27c97542fae111b54526326b8a93fcf7ca6 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Routines/MigrateActivityLogDb.cs | 18 +++++++++++++++++- .../Migrations/Routines/MigrateUserDb.cs | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index a954d307e1..b36db347cd 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -55,9 +55,25 @@ namespace Jellyfin.Server.Migrations.Routines }; var dataPath = _paths.DataPath; - using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) + var activityLogPath = Path.Combine(dataPath, DbFilename); + if (!File.Exists(activityLogPath)) + { + _logger.LogWarning("{ActivityLogDb} doesn't exist, nothing to migrate", activityLogPath); + return; + } + + using (var connection = new SqliteConnection($"Filename={activityLogPath}")) { connection.Open(); + var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='ActivityLog';"); + foreach (var row in tableQuery) + { + if (row.GetInt32(0) == 0) + { + _logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath); + break; + } + } using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}"); userDbConnection.Open(); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index e5584fb947..c3f07c0899 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -57,11 +57,28 @@ public class MigrateUserDb : IMigrationRoutine public void Perform() { var dataPath = _paths.DataPath; + var userDbPath = Path.Combine(dataPath, DbFilename); + if (!File.Exists(userDbPath)) + { + _logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath); + return; + } + _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin."); - using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) + using (var connection = new SqliteConnection($"Filename={userDbPath}")) { connection.Open(); + var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';"); + foreach (var row in tableQuery) + { + if (row.GetInt32(0) == 0) + { + _logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath); + break; + } + } + using var dbContext = _provider.CreateDbContext(); var queryResult = connection.Query("SELECT * FROM LocalUsersv2"); From 28b0657608e144dead4100e6b3d89ccea763a5cf Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:30 -0400 Subject: [PATCH 048/300] Backport pull request #15217 from jellyfin/release-10.11.z Normalize paths in database queries Original-merge: 75f472e6a78a7516927078238d102f9eff95b7a3 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 8319bfd944..b939c4ab21 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1763,7 +1763,8 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.Path)) { - baseQuery = baseQuery.Where(e => e.Path == filter.Path); + var pathToQuery = GetPathToSave(filter.Path); + baseQuery = baseQuery.Where(e => e.Path == pathToQuery); } if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey)) From 21f65e2e27720b43ac7d42ccf645c69c3d619bc1 Mon Sep 17 00:00:00 2001 From: JJBlue <JJBlue@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:43:31 -0400 Subject: [PATCH 049/300] Backport pull request #15220 from jellyfin/release-10.11.z Skip extracted files in migration if bad timestamp or no access Original-merge: a305204cfa43a97f255f0dea412f93d02de939c3 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Migrations/Routines/MigrateKeyframeData.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index 612da05214..aa55309264 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -122,6 +122,16 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine { lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath); } + catch (ArgumentOutOfRangeException e) + { + _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message); + return null; + } + catch (UnauthorizedAccessException e) + { + _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message); + return null; + } catch (IOException e) { _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message); From 7d778d7befd271f08d202494c92bc9c3666c18ec Mon Sep 17 00:00:00 2001 From: Pascal Wiesmann <pascal-wwe@hotmail.de> Date: Mon, 27 Oct 2025 18:03:16 -0400 Subject: [PATCH 050/300] Translated using Weblate (Alemannic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/gsw/ --- Emby.Server.Implementations/Localization/Core/gsw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index f847d83d14..e1ee8cf7c4 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -11,7 +11,7 @@ "Collections": "Sammlungen", "DeviceOfflineWithName": "{0} wurde getrennt", "DeviceOnlineWithName": "{0} ist verbunden", - "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}", + "FailedLoginAttemptWithUserName": "Fählgschlagene Ameldeversuech vo {0}", "Favorites": "Favorite", "Folders": "Ordner", "Genres": "Genre", From f92eca3efb2b01b5c5b4ab862ede2620bdf214c9 Mon Sep 17 00:00:00 2001 From: Battseren Badral <bbattseren88@gmail.com> Date: Tue, 28 Oct 2025 14:43:19 -0400 Subject: [PATCH 051/300] Translated using Weblate (Mongolian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/ --- Emby.Server.Implementations/Localization/Core/mn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index cf39df706d..ec553f5bed 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -111,7 +111,7 @@ "Shows": "Шоу", "Sync": "Дахин", "System": "Систем", - "TvShows": "Цуварлууд", + "TvShows": "ТВ нэвтрүүлгүүд", "Undefined": "Танисангүй", "User": "Хэрэглэгч", "UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ", From f21fe9f95ea8f8b0da5f7ce315feab4c0b4925dd Mon Sep 17 00:00:00 2001 From: Jacky He <hekinghung@gmail.com> Date: Wed, 29 Oct 2025 17:13:58 -0400 Subject: [PATCH 052/300] 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 39141d8416..5e17e46475 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -137,5 +137,6 @@ "TaskCleanCollectionsAndPlaylistsDescription": "從資料庫及播放清單中移除已不存在的項目。", "TaskMoveTrickplayImagesDescription": "根據媒體庫設定移動現有的 Trickplay 檔案。", "TaskMoveTrickplayImages": "轉移 Trickplay 影像位置", - "CleanupUserDataTask": "用戶資料清理工作" + "CleanupUserDataTask": "用戶資料清理工作", + "CleanupUserDataTaskDescription": "從用戶數據中清除已經被刪除超過 90 日的媒體相關資料。" } From 573ce9ceaac41d35b9e9431457e66ce06bf17e77 Mon Sep 17 00:00:00 2001 From: Battseren Badral <bbattseren88@gmail.com> Date: Wed, 29 Oct 2025 16:20:27 -0400 Subject: [PATCH 053/300] Translated using Weblate (Mongolian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mn/ --- Emby.Server.Implementations/Localization/Core/mn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/mn.json b/Emby.Server.Implementations/Localization/Core/mn.json index ec553f5bed..a684ff2041 100644 --- a/Emby.Server.Implementations/Localization/Core/mn.json +++ b/Emby.Server.Implementations/Localization/Core/mn.json @@ -109,7 +109,7 @@ "ScheduledTaskStartedWithName": "{0}-г эхлүүлэв", "ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу", "Shows": "Шоу", - "Sync": "Дахин", + "Sync": "Синхрончлох", "System": "Систем", "TvShows": "ТВ нэвтрүүлгүүд", "Undefined": "Танисангүй", From 83d0dbdbcbe02382863dd48ad15b88ad2f48cb89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:35:55 +0000 Subject: [PATCH 054/300] Update github/codeql-action action to v4.31.2 --- .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 53daf0991f..3cb35908bc 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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 From 23929a3e709f4324d49271c02b0b047e1149e860 Mon Sep 17 00:00:00 2001 From: Jacky He <hekinghung@gmail.com> Date: Thu, 30 Oct 2025 12:24:01 -0400 Subject: [PATCH 055/300] 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 5e17e46475..60af2274e4 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "最愛的節目", "HeaderFavoriteSongs": "最愛的歌曲", "HeaderLiveTV": "電視直播", - "HeaderNextUp": "接著播放", + "HeaderNextUp": "繼續觀看", "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", From ffe82be7a7a2ea6ab6d0e543f58ecd01df4085ef Mon Sep 17 00:00:00 2001 From: Jacky He <hekinghung@gmail.com> Date: Fri, 31 Oct 2025 17:12:15 -0400 Subject: [PATCH 056/300] 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 60af2274e4..c8800e256e 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -127,8 +127,8 @@ "HearingImpaired": "聽力障礙", "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。", - "TaskExtractMediaSegments": "掃描媒體段落", - "TaskExtractMediaSegmentsDescription": "從MediaSegment中被允許的插件獲取媒體段落。", + "TaskExtractMediaSegments": "掃描媒體分段資訊", + "TaskExtractMediaSegmentsDescription": "從允許MediaSegment 功能的插件中獲取媒體片段。", "TaskDownloadMissingLyrics": "下載欠缺歌詞", "TaskDownloadMissingLyricsDescription": "下載歌詞", "TaskCleanCollectionsAndPlaylists": "整理媒體與播放清單", From 63f06aad9420b31a1cc79052b0408fb8c96e1c72 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Nov 2025 23:14:23 +0000 Subject: [PATCH 057/300] Update dependency Serilog.Sinks.Console to 6.1.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 12aab38288..5f11271c1e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -68,7 +68,7 @@ <PackageVersion Include="Serilog.Expressions" Version="5.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" /> - <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" /> + <PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" /> <PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> From 4258df4485c928e5a128bfb7c0149ba51b7b33af Mon Sep 17 00:00:00 2001 From: nyanmisaka <nst799610810@gmail.com> Date: Sun, 2 Nov 2025 21:58:41 -0500 Subject: [PATCH 058/300] Backport pull request #15247 from jellyfin/release-10.11.z Ignore initial delay in audio-only containers Original-merge: 6bf88c049e9f64ec5829d60d69eff3c3239a9528 Merged-by: crobibero <cody@robibe.ro> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 2614fe9956..fe6f855b5e 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1625,8 +1625,11 @@ public class DynamicHlsController : BaseJellyfinApiController var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions; - // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT - hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; + if (state.VideoStream is not null && state.IsOutputVideo) + { + // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT + hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont"; + } segmentFormat = "fmp4" + outputFmp4HeaderArg; } From 1ccd10863e24485978a2f5fd7650b2bcf42168d3 Mon Sep 17 00:00:00 2001 From: thornbill <thornbill@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:58:42 -0500 Subject: [PATCH 059/300] Backport pull request #15254 from jellyfin/release-10.11.z Update password reset to always return the same response structure Original-merge: 4ad31418753840ca76c52fc2aa56fa1a4235ca87 Merged-by: crobibero <cody@robibe.ro> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- .../Users/DefaultPasswordResetProvider.cs | 40 +++++++++++-------- .../Users/UserManager.cs | 24 +++++------ .../Authentication/IPasswordResetProvider.cs | 5 +-- .../Users/ForgotPasswordAction.cs | 4 ++ 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index f20fb2d92d..49a9fda943 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Security.Cryptography; using System.Text.Json; @@ -92,33 +93,38 @@ namespace Jellyfin.Server.Implementations.Users } /// <inheritdoc /> - public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork) + public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork) { - byte[] bytes = new byte[4]; - RandomNumberGenerator.Fill(bytes); - string pin = BitConverter.ToString(bytes); - DateTime expireTime = DateTime.UtcNow.AddMinutes(30); - string filePath = _passwordResetFileBase + user.Id + ".json"; - SerializablePasswordReset spr = new SerializablePasswordReset - { - ExpirationDate = expireTime, - Pin = pin, - PinFile = filePath, - UserName = user.Username - }; + var usernameHash = enteredUsername.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); + var pinFile = _passwordResetFileBase + usernameHash + ".json"; - FileStream fileStream = AsyncFile.Create(filePath); - await using (fileStream.ConfigureAwait(false)) + if (user is not null && isInNetwork) { - await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); + byte[] bytes = new byte[4]; + RandomNumberGenerator.Fill(bytes); + string pin = BitConverter.ToString(bytes); + + SerializablePasswordReset spr = new SerializablePasswordReset + { + ExpirationDate = expireTime, + Pin = pin, + PinFile = pinFile, + UserName = user.Username + }; + + FileStream fileStream = AsyncFile.Create(pinFile); + await using (fileStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); + } } return new ForgotPasswordResult { Action = ForgotPasswordAction.PinCode, PinExpirationDate = expireTime, - PinFile = filePath + PinFile = pinFile }; } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index d0b41a7f6b..b534ccd1bd 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -508,23 +508,18 @@ namespace Jellyfin.Server.Implementations.Users public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork) { var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername); + var passwordResetProvider = GetPasswordResetProvider(user); + + var result = await passwordResetProvider + .StartForgotPasswordProcess(user, enteredUsername, isInNetwork) + .ConfigureAwait(false); if (user is not null && isInNetwork) { - var passwordResetProvider = GetPasswordResetProvider(user); - var result = await passwordResetProvider - .StartForgotPasswordProcess(user, isInNetwork) - .ConfigureAwait(false); - await UpdateUserAsync(user).ConfigureAwait(false); - return result; } - return new ForgotPasswordResult - { - Action = ForgotPasswordAction.InNetworkRequired, - PinFile = string.Empty - }; + return result; } /// <inheritdoc/> @@ -760,8 +755,13 @@ namespace Jellyfin.Server.Implementations.Users return GetAuthenticationProviders(user)[0]; } - private IPasswordResetProvider GetPasswordResetProvider(User user) + private IPasswordResetProvider GetPasswordResetProvider(User? user) { + if (user is null) + { + return _defaultPasswordResetProvider; + } + return GetPasswordResetProviders(user)[0]; } diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 592ce99556..36cd5c5d14 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -15,11 +13,12 @@ namespace MediaBrowser.Controller.Authentication bool IsEnabled { get; } - Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork); + Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork); Task<PinRedeemResult> RedeemPasswordResetPin(string pin); } +#nullable disable public class PasswordPinCreationResult { public string PinFile { get; set; } diff --git a/MediaBrowser.Model/Users/ForgotPasswordAction.cs b/MediaBrowser.Model/Users/ForgotPasswordAction.cs index f198476e3b..55907e6c84 100644 --- a/MediaBrowser.Model/Users/ForgotPasswordAction.cs +++ b/MediaBrowser.Model/Users/ForgotPasswordAction.cs @@ -1,11 +1,15 @@ #pragma warning disable CS1591 +using System; + namespace MediaBrowser.Model.Users { public enum ForgotPasswordAction { + [Obsolete("Returning different actions represents a security concern.")] ContactAdmin = 0, PinCode = 1, + [Obsolete("Returning different actions represents a security concern.")] InNetworkRequired = 2 } } From c9d93b0745381154d81023e0602bb8bbd7566418 Mon Sep 17 00:00:00 2001 From: crobibero <cody@robibe.ro> Date: Sun, 2 Nov 2025 21:58:43 -0500 Subject: [PATCH 060/300] Backport pull request #15322 from jellyfin/release-10.11.z Fix legacy migration file checks Original-merge: da254ee968deca4d47f0f5d1164c5e883745ac60 Merged-by: crobibero <cody@robibe.ro> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- .../Routines/MigrateActivityLogDb.cs | 2 +- .../Routines/MigrateAuthenticationDb.cs | 21 ++++++++++++++++++- .../Routines/MigrateDisplayPreferencesDb.cs | 18 ++++++++++++++++ .../Migrations/Routines/MigrateUserDb.cs | 2 +- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index b36db347cd..8c8563190d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -71,7 +71,7 @@ namespace Jellyfin.Server.Migrations.Routines if (row.GetInt32(0) == 0) { _logger.LogWarning("Table 'ActivityLog' doesn't exist in {ActivityLogPath}, nothing to migrate", activityLogPath); - break; + return; } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index c6699c21df..0de775e03a 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -50,9 +50,28 @@ namespace Jellyfin.Server.Migrations.Routines public void Perform() { var dataPath = _appPaths.DataPath; - using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}")) + var dbFilePath = Path.Combine(dataPath, DbFilename); + + if (!File.Exists(dbFilePath)) + { + _logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath); + return; + } + + using (var connection = new SqliteConnection($"Filename={dbFilePath}")) { connection.Open(); + + var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='Tokens';"); + foreach (var row in tableQuery) + { + if (row.GetInt32(0) == 0) + { + _logger.LogWarning("Table 'Tokens' doesn't exist in {Path}, nothing to migrate", dbFilePath); + return; + } + } + using var dbContext = _dbProvider.CreateDbContext(); var authenticatedDevices = connection.Query("SELECT * FROM Tokens"); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 0d9952ce97..ffd06fea0d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -78,9 +78,27 @@ namespace Jellyfin.Server.Migrations.Routines var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var dbFilePath = Path.Combine(_paths.DataPath, DbFilename); + + if (!File.Exists(dbFilePath)) + { + _logger.LogWarning("{Path} doesn't exist, nothing to migrate", dbFilePath); + return; + } + using (var connection = new SqliteConnection($"Filename={dbFilePath}")) { connection.Open(); + + var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='userdisplaypreferences';"); + foreach (var row in tableQuery) + { + if (row.GetInt32(0) == 0) + { + _logger.LogWarning("Table 'userdisplaypreferences' doesn't exist in {Path}, nothing to migrate", dbFilePath); + return; + } + } + using var dbContext = _provider.CreateDbContext(); var results = connection.Query("SELECT * FROM userdisplaypreferences"); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index c3f07c0899..8c3361ee16 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -75,7 +75,7 @@ public class MigrateUserDb : IMigrationRoutine if (row.GetInt32(0) == 0) { _logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath); - break; + return; } } From 871de372ff89597c9b1e9ca10ff52e2b0a8a7983 Mon Sep 17 00:00:00 2001 From: vinnyspb <vinnyspb@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:58:44 -0500 Subject: [PATCH 061/300] Backport pull request #15325 from jellyfin/release-10.11.z Update file size when refreshing metadata Original-merge: f994dd62114b17c335d508c8e5709f24009eb16e Merged-by: crobibero <cody@robibe.ro> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- MediaBrowser.Providers/Manager/MetadataService.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 1d83263c5e..4c83845992 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -229,6 +229,11 @@ namespace MediaBrowser.Providers.Manager if (file is not null) { item.DateModified = file.LastWriteTimeUtc; + + if (!file.IsDirectory) + { + item.Size = file.Length; + } } } From 87b02b13166b5dc8cd59e273e1c82339db940a27 Mon Sep 17 00:00:00 2001 From: Shadowghost <Shadowghost@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:58:45 -0500 Subject: [PATCH 062/300] Backport pull request #15326 from jellyfin/release-10.11.z Skip too large extracted season numbers Original-merge: e7dbb3afec3282c556e4fe35d9376ecaa4417171 Merged-by: crobibero <cody@robibe.ro> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- Emby.Naming/TV/SeasonPathParser.cs | 6 ++++-- tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 90aae2d485..eafb09a6a3 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -113,8 +113,10 @@ namespace Emby.Naming.TV var numberString = match.Groups["seasonnumber"]; if (numberString.Success) { - var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture); - return (seasonNumber, true); + if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber)) + { + return (seasonNumber, true); + } } return (null, false); diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 7671166ff4..0c3671f4fb 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -66,6 +66,9 @@ public class SeasonPathParserTests [InlineData("/Drive/SPECIALS", "/Drive", 0, true)] [InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)] [InlineData("/Drive/Episode 1 SEASON 2", "/Drive", null, false)] + [InlineData("/media/YouTube/Devyn Johnston/2024-01-24 4070 Ti SUPER in under 7 minutes", "/media/YouTube/Devyn Johnston", null, false)] + [InlineData("/media/YouTube/Devyn Johnston/2025-01-28 5090 vs 2 SFF Cases", "/media/YouTube/Devyn Johnston", null, false)] + [InlineData("/Drive/202401244070", "/Drive", null, false)] public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, parentPath, true, true); From a7d039b7c63b0ea9c5bc69898baa1e69d9396908 Mon Sep 17 00:00:00 2001 From: evanreichard <30810613+evanreichard@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:58:46 -0500 Subject: [PATCH 063/300] Backport pull request #15328 from jellyfin/release-10.11.z fix: in optimistic locking, key off table is locked Original-merge: b5f0199a25cc221ff86d112ed6968a5352277e32 Merged-by: crobibero <cody@robibe.ro> Backported-by: Joshua M. Boniface <joshua@boniface.me> --- .../Locking/OptimisticLockBehavior.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs index b90a2e056f..7bcc7eeca4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs @@ -52,10 +52,14 @@ public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior _logger = logger; _writePolicy = Policy - .HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase)) + .HandleInner<Exception>(e => + e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) || + e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase)) .WaitAndRetry(sleepDurations.Length, backoffProvider, RetryHandle); _writeAsyncPolicy = Policy - .HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase)) + .HandleInner<Exception>(e => + e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase) || + e.Message.Contains("database table is locked", StringComparison.InvariantCultureIgnoreCase)) .WaitAndRetryAsync(sleepDurations.Length, backoffProvider, RetryHandle); void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context) From f693c9d39f00317e33153061a0c406aa8a607555 Mon Sep 17 00:00:00 2001 From: Diogo Coelho <diogoamcoelho16@gmail.com> Date: Thu, 6 Nov 2025 04:13:50 -0500 Subject: [PATCH 064/300] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index f188822d6a..c3eba362d2 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -5,7 +5,7 @@ "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Books": "Livros", - "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}", + "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}", "Channels": "Canais", "ChapterNameValue": "Capítulo {0}", "Collections": "Coleções", 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 065/300] 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 066/300] 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 e69754fd3ad67e1687d07a319801c09d70ad55d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 8 Nov 2025 04:18:07 +0000 Subject: [PATCH 067/300] Update appleboy/ssh-action action to v1.2.3 --- .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 d37602690d..7125ecd419 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@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 + uses: appleboy/ssh-action@91f3272fc5907f4699dcf59761eb622a07342f5a # v1.2.3 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@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 + uses: appleboy/ssh-action@91f3272fc5907f4699dcf59761eb622a07342f5a # v1.2.3 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" From 0d7b687da092d32efbf12b4e43a905c6c066e3de Mon Sep 17 00:00:00 2001 From: Andrew <dpieski@gmail.com> Date: Sat, 8 Nov 2025 10:30:30 -0500 Subject: [PATCH 068/300] Update Jellyfin Server version in issue template (#15398) --- .github/ISSUE_TEMPLATE/issue report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 2699688395..d304fb7291 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,7 +87,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: - - 10.10.0+ + - 10.11.0+ - Master - Unstable - Older* From 32fc57cf172d5d8bd9a6c43c6de36a8a64e9fbf8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 10:59:00 +0000 Subject: [PATCH 069/300] Update dependency z440.atl.core to 7.7.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5f11271c1e..954421245c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,7 +88,7 @@ <PackageVersion Include="System.Text.Json" Version="9.0.10" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.6.0" /> + <PackageVersion Include="z440.atl.core" Version="7.7.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 3d7a68beb13007520e7b0fac48e93e7de113aa43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:40:14 +0000 Subject: [PATCH 070/300] Update dependency FsCheck.Xunit to 3.3.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5f11271c1e..90e6550a53 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ <PackageVersion Include="Diacritics" Version="4.0.17" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="FsCheck.Xunit" Version="3.3.1" /> + <PackageVersion Include="FsCheck.Xunit" Version="3.3.2" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" /> <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" /> From b8a409195fd7a6e18063325b211b6afa3a377581 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:42:10 +0000 Subject: [PATCH 071/300] Update dependency dotnet-ef to v9.0.11 --- .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 df2b50e269..029a48f6a1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.10", + "version": "9.0.11", "commands": [ "dotnet-ef" ] From 8471a67bcd6f95451c2e8afda886ea4cbc50bc7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 18:42:18 +0000 Subject: [PATCH 072/300] Update Microsoft --- Directory.Packages.props | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 163e67b510..bc4fac68ab 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,33 +26,33 @@ <PackageVersion Include="libse" Version="4.0.12" /> <PackageVersion Include="LrcParser" Version="2025.623.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.10" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" /> + <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" /> <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.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.10" /> - <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.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.NET.Test.Sdk" Version="18.0.1" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Moq" Version="4.18.4" /> @@ -84,9 +84,9 @@ <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <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.10" /> - <PackageVersion Include="System.Text.Json" Version="9.0.10" /> - <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" /> + <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="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="z440.atl.core" Version="7.7.0" /> <PackageVersion Include="TMDbLib" Version="2.3.0" /> From b216a27bfcb2476bb346e8f003dec3b6cbc91991 Mon Sep 17 00:00:00 2001 From: Grant Alexander <grantwa710@outlook.com> Date: Wed, 12 Nov 2025 17:32:35 -0500 Subject: [PATCH 073/300] Translated using Weblate (English (Pirate)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/ --- Emby.Server.Implementations/Localization/Core/pr.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 9076b9c878..7b1cd886f0 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -16,7 +16,7 @@ "Collections": "Barrels", "ItemAddedWithName": "{0} is now with yer treasure", "Default": "Normal-like", - "FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}", + "FailedLoginAttemptWithUserName": "Ye failed to enter from {0}", "Favorites": "Finest Loot", "ItemRemovedWithName": "{0} was taken from yer treasure", "LabelIpAddressValue": "Ship's coordinates: {0}", @@ -113,5 +113,10 @@ "TaskCleanCache": "Sweep the Cache Chest", "TaskRefreshChapterImages": "Claim chapter portraits", "TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.", - "TaskRefreshLibrary": "Scan the Treasure Trove" + "TaskRefreshLibrary": "Scan the Treasure Trove", + "TasksChannelsCategory": "Channels of the Internet", + "TaskRefreshTrickplayImages": "Summon the picture tricks", + "TaskRefreshTrickplayImagesDescription": "Summons picture trick previews for videos in ye enabled book roost", + "TaskUpdatePlugins": "Resummon yer Plugins", + "TaskCleanTranscode": "Swab Ye Transcode Directory" } From 931240a3f58df261c161c71768b6c805c1c6f659 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 01:24:06 +0000 Subject: [PATCH 074/300] 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 3cb35908bc..8c5f85dd5b 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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index b9fdd456f1..e48b2d5665 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@f3c6b3f8a29686284ef7a7cf6dccb79a01d98444 # v5.4.18 + uses: danielpalme/ReportGenerator-GitHub-Action@dcdfb6e704e87df6b2ed0cf123a6c9f69e364869 # v5.5.0 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From d1722936c0a453c5b70bca96a8f148b11d2e0688 Mon Sep 17 00:00:00 2001 From: hoanghuy309 <hoanghuy309@gmail.com> Date: Fri, 14 Nov 2025 23:56:11 -0500 Subject: [PATCH 075/300] Translated using Weblate (Vietnamese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/vi/ --- Emby.Server.Implementations/Localization/Core/vi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index d1c5166cb1..3f4bf1f7ff 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -39,7 +39,7 @@ "TasksMaintenanceCategory": "Bảo Trì", "VersionNumber": "Phiên Bản {0}", "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn", - "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}", + "UserStoppedPlayingItemWithValues": "{0} đã kết thúc phát {1} trên {2}", "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}", "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}", "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}", From 52f0c3dd24bdf1cd16cfcecfef0265206ed08c65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:53:55 +0000 Subject: [PATCH 076/300] Update dependency z440.atl.core to 7.8.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 163e67b510..1519462800 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -88,7 +88,7 @@ <PackageVersion Include="System.Text.Json" Version="9.0.10" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.7.0" /> + <PackageVersion Include="z440.atl.core" Version="7.8.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 5182aec13f853839cddb74ae0ebcc6b35009dc04 Mon Sep 17 00:00:00 2001 From: Karolis <karoliscd@gmail.com> Date: Mon, 17 Nov 2025 15:18:29 +0200 Subject: [PATCH 077/300] 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 626ab7e00a91e145475b50a0461138057f7ee70e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:40:00 +0000 Subject: [PATCH 078/300] Update actions/checkout action to v5.0.1 --- .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 | 4 ++-- .github/workflows/issue-template-check.yml | 2 +- .github/workflows/release-bump-version.yaml | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 8c5f85dd5b..cbab3efea8 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: Setup .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 0651233386..d78af466a0 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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 7125ecd419..10cc386865 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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 e48b2d5665..0f47e8ca83 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index ba12d47473..13fc61d21b 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: repository: jellyfin/jellyfin-triage-script - name: install python diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index b49647d337..fb868be4ce 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: repository: jellyfin/jellyfin-triage-script - name: install python diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml index ec91744f32..943cc3f4fc 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ env.TAG_BRANCH }} From 06fb300cff5d63404a888209ab82b192b9e46be4 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:45 -0500 Subject: [PATCH 079/300] Backport pull request #14955 from jellyfin/release-10.11.z Fix tmdbid not detected in single movie folder Original-merge: def5956cd1afe8848c0e232fa477720c4158832f Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Library/Resolvers/Movies/MovieResolver.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 333c8c34bf..98e8f5350b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -369,13 +369,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies // We need to only look at the name of this actual item (not parents) var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan()); - if (!justName.IsEmpty) + var tmdbid = justName.GetAttributeValue("tmdbid"); + + // If not in a mixed folder and ID not found in folder path, check filename + if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder) { - // Check for TMDb id - var tmdbid = justName.GetAttributeValue("tmdbid"); - item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid); + tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid"); } + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid); + if (!string.IsNullOrEmpty(item.Path)) { // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) From 5ea3910af96ead74d9267ec239e47a798a33e78f Mon Sep 17 00:00:00 2001 From: revam <revam@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:47 -0500 Subject: [PATCH 080/300] Backport pull request #15263 from jellyfin/release-10.11.z Resolve symlinks for static media source infos Original-merge: 3b2d64995aab63ebaa6832c059a3cc0bdebe90dc Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../IO/ManagedFileSystem.cs | 3 +- .../Library/DotIgnoreIgnoreRule.cs | 3 +- .../Trickplay/TrickplayManager.cs | 4 +- MediaBrowser.Controller/Entities/BaseItem.cs | 12 ++- .../IO/FileSystemHelper.cs | 74 +++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 97e89ca3d9..fad97344b5 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -260,7 +261,7 @@ namespace Emby.Server.Implementations.IO { try { - var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true); + var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true); if (targetFileInfo is not null) { result.Exists = targetFileInfo.Exists; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 959acd4751..e53502046a 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,7 @@ using System; using System.IO; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.IO; @@ -92,7 +93,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule private static string GetFileContent(FileInfo dirIgnoreFile) { - dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile; + dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; if (!dirIgnoreFile.Exists) { return string.Empty; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 6f2d2a1071..4505a377ce 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager } // We support video backdrops, but we should not generate trickplay images for them - var parentDirectory = Directory.GetParent(mediaPath); + var parentDirectory = Directory.GetParent(video.Path); if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id); + _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id); return; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4989f0f3f6..3c46d53e5c 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,6 +24,7 @@ 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; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; @@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities var protocol = item.PathProtocol; + // Resolve the item path so everywhere we use the media source it will always point to + // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link + // path will return null, so it's safe to check for all paths. + var itemPath = item.Path; + if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo) + { + itemPath = linkInfo.FullName; + } + var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), @@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), - Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 1a33c3aa8c..324aea7e3b 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Model.IO; @@ -61,4 +62,77 @@ public static class FileSystemHelper } } } + + /// <summary> + /// Gets the target of the specified file link. + /// </summary> + /// <remarks> + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// </remarks> + /// <param name="linkPath">The path of the file link.</param> + /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param> + /// <returns> + /// A <see cref="FileInfo"/> if the <paramref name="linkPath"/> is a link, regardless of if the target exists; otherwise, <c>null</c>. + /// </returns> + public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + // Check if the file exists so the native resolve handler won't throw at us. + if (!File.Exists(linkPath)) + { + return null; + } + + if (!returnFinalTarget) + { + return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + } + + if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) + { + return null; + } + + var currentPath = targetInfo.FullName; + var visited = new HashSet<string>(StringComparer.Ordinal) { linkPath, currentPath }; + while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + { + var targetPath = linkInfo.FullName; + + // If an infinite loop is detected, return the file info for the + // first link in the loop we encountered. + if (!visited.Add(targetPath)) + { + return new FileInfo(targetPath); + } + + targetInfo = linkInfo; + currentPath = targetPath; + + // Exit if the target doesn't exist, so the native resolve handler won't throw at us. + if (!targetInfo.Exists) + { + break; + } + } + + return targetInfo; + } + + /// <summary> + /// Gets the target of the specified file link. + /// </summary> + /// <remarks> + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// </remarks> + /// <param name="fileInfo">The file info of the file link.</param> + /// <param name="returnFinalTarget">true to follow links to the final target; false to return the immediate next link.</param> + /// <returns> + /// A <see cref="FileInfo"/> if the <paramref name="fileInfo"/> is a link, regardless of if the target exists; otherwise, <c>null</c>. + /// </returns> + public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false) + { + ArgumentNullException.ThrowIfNull(fileInfo); + + return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget); + } } From c2c4c0adbfbbabb5e806cc08207aef00c409dac9 Mon Sep 17 00:00:00 2001 From: evanreichard <30810613+evanreichard@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:48 -0500 Subject: [PATCH 081/300] Backport pull request #15369 from jellyfin/release-10.11.z feat(sqlite): add timeout config Original-merge: c2e5081d64e519a74d47df23335bb228fea8ec7e Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 2b000b257b..da63df8e29 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -64,6 +64,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default); sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true); + sqliteConnectionBuilder.DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30); var connectionString = sqliteConnectionBuilder.ToString(); From c805c5e2b15698cebb9ebd01a961932cc6c7f075 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:49 -0500 Subject: [PATCH 082/300] Backport pull request #15373 from jellyfin/release-10.11.z Fix collection grouping in mixed libraries Original-merge: 13c4517a66e3f857cc4acc9b2fa3505297d554eb Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/Folder.cs | 75 ++++++++++++++++------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 03ee447088..ed2d201e32 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1052,12 +1052,49 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(items); - if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) + if (!CollapseBoxSetItems(query, queryParent, user, configurationManager)) { - items = collectionManager.CollapseItemsWithinBoxSets(items, user); + return items; } - return items; + var config = configurationManager.Configuration; + + bool collapseMovies = config.EnableGroupingMoviesIntoCollections; + bool collapseSeries = config.EnableGroupingShowsIntoCollections; + + if (user is null || (collapseMovies && collapseSeries)) + { + return collectionManager.CollapseItemsWithinBoxSets(items, user); + } + + if (!collapseMovies && !collapseSeries) + { + return items; + } + + var collapsibleItems = new List<BaseItem>(); + var remainingItems = new List<BaseItem>(); + + foreach (var item in items) + { + if ((collapseMovies && item is Movie) || (collapseSeries && item is Series)) + { + collapsibleItems.Add(item); + } + else + { + remainingItems.Add(item); + } + } + + if (collapsibleItems.Count == 0) + { + return remainingItems; + } + + var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user); + + return collapsedItems.Concat(remainingItems); } private static bool CollapseBoxSetItems( @@ -1088,24 +1125,26 @@ namespace MediaBrowser.Controller.Entities } var param = query.CollapseBoxSetItems; - - if (!param.HasValue) + if (param.HasValue) { - if (user is not null && query.IncludeItemTypes.Any(type => - (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) || - (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections))) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series)) - { - param = true; - } + return param.Value && AllowBoxSetCollapsing(query); } - return param.HasValue && param.Value && AllowBoxSetCollapsing(query); + var config = configurationManager.Configuration; + + bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie); + bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series); + + bool collapseMovies = config.EnableGroupingMoviesIntoCollections; + bool collapseSeries = config.EnableGroupingShowsIntoCollections; + + if (user is not null) + { + bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries); + return canCollapse && AllowBoxSetCollapsing(query); + } + + return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query); } private static bool AllowBoxSetCollapsing(InternalItemsQuery request) From 7d05c875f3865d0baa98a9a3dcfd05842354a764 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:50 -0500 Subject: [PATCH 083/300] Backport pull request #15380 from jellyfin/release-10.11.z Fix item count display for collapsed items Original-merge: 8f71922734d42591b3236f4c52d9692f1b191da2 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- MediaBrowser.Controller/Entities/Folder.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index ed2d201e32..151b957fe9 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -729,9 +729,7 @@ namespace MediaBrowser.Controller.Entities query.StartIndex = startIndex; } - var result = PostFilterAndSort(items, query); - result.TotalRecordCount = totalCount; - return result; + return PostFilterAndSort(items, query); } if (this is not UserRootFolder @@ -1001,9 +999,7 @@ namespace MediaBrowser.Controller.Entities items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); } - var result = PostFilterAndSort(items, query); - result.TotalRecordCount = totalItemCount; - return result; + return PostFilterAndSort(items, query); } protected QueryResult<BaseItem> PostFilterAndSort(IEnumerable<BaseItem> items, InternalItemsQuery query) @@ -1039,7 +1035,15 @@ namespace MediaBrowser.Controller.Entities items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } - return UserViewBuilder.SortAndPage(items, null, query, LibraryManager); + var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList(); + var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager); + + if (query.EnableTotalRecordCount) + { + result.TotalRecordCount = filteredItems.Count; + } + + return result; } private static IEnumerable<BaseItem> CollapseBoxSetItemsIfNeeded( From bcec5f2e445c566fafab3bc93e0765679b6dc83c Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:51 -0500 Subject: [PATCH 084/300] Backport pull request #15381 from jellyfin/release-10.11.z Fix name filters to use only SortName Original-merge: 7222910b05dff772fad22d4f557fad20578fa275 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b939c4ab21..06fb8ec217 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1944,19 +1944,20 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) { - baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith)); + var startsWithLower = filter.NameStartsWith.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower)); } if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]); + var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0); } if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]); + var lessThanLower = filter.NameLessThan.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0); } if (filter.ImageTypes.Length > 0) From f848b8f12cc410ca5e8516762a8465280da8974d Mon Sep 17 00:00:00 2001 From: Bond-009 <bond.009@outlook.com> Date: Mon, 17 Nov 2025 14:08:53 -0500 Subject: [PATCH 085/300] Backport pull request #15390 from jellyfin/release-10.11.z Don't enforce a minimum amount of free space for the tmp and log dirs Original-merge: 097cb87f6f6df662361a4cd536b56470e4cd68a3 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../StorageHelpers/StorageHelper.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index 570d6cb9b7..ce628a04d0 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -13,7 +13,6 @@ namespace Jellyfin.Server.Implementations.StorageHelpers; public static class StorageHelper { private const long TwoGigabyte = 2_147_483_647L; - private const long FiveHundredAndTwelveMegaByte = 536_870_911L; private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; /// <summary> @@ -24,10 +23,8 @@ public static class StorageHelper public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger) { TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte); - TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte); TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte); TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte); - TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte); } /// <summary> From 6c5f44878756414716b553cf66767a988816a2a8 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:54 -0500 Subject: [PATCH 086/300] Backport pull request #15404 from jellyfin/release-10.11.z Improve season folder parsing Original-merge: 2e5ced50986c37b19b5f4ef34d730fc56a51535a Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Emby.Naming/TV/SeasonPathParser.cs | 55 +++++++++---------- .../TV/SeasonPathParserTests.cs | 6 ++ 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index eafb09a6a3..72adfb2d96 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -10,12 +10,17 @@ namespace Emby.Naming.TV /// </summary> public static partial class SeasonPathParser { + private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled); + [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPre(); - [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$", RegexOptions.IgnoreCase)] + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPost(); + [GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)] + private static partial Regex SeasonPrefix(); + /// <summary> /// Attempts to parse season number from path. /// </summary> @@ -56,44 +61,34 @@ namespace Emby.Naming.TV bool supportSpecialAliases, bool supportNumericSeasonFolders) { - string filename = Path.GetFileName(path); - filename = Regex.Replace(filename, "[ ._-]", string.Empty); + var fileName = Path.GetFileName(path); + + var seasonPrefixMatch = SeasonPrefix().Match(fileName); + if (seasonPrefixMatch.Success && + int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + return (val, true); + } + + string filename = CleanNameRegex.Replace(fileName, string.Empty); if (parentFolderName is not null) { - parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); - filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); + var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty); + filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase); } - if (supportSpecialAliases) + if (supportSpecialAliases && + (filename.Equals("specials", StringComparison.OrdinalIgnoreCase) || + filename.Equals("extras", StringComparison.OrdinalIgnoreCase))) { - if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) - { - return (0, true); - } - - if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase)) - { - return (0, true); - } + return (0, true); } - if (supportNumericSeasonFolders) + if (supportNumericSeasonFolders && + int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) { - if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } - } - - if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's')) - { - var testFilename = filename.AsSpan()[1..]; - - if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } + return (val, true); } var preMatch = ProcessPre().Match(filename); diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 0c3671f4fb..4dbe769bf4 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -69,6 +69,12 @@ public class SeasonPathParserTests [InlineData("/media/YouTube/Devyn Johnston/2024-01-24 4070 Ti SUPER in under 7 minutes", "/media/YouTube/Devyn Johnston", null, false)] [InlineData("/media/YouTube/Devyn Johnston/2025-01-28 5090 vs 2 SFF Cases", "/media/YouTube/Devyn Johnston", null, false)] [InlineData("/Drive/202401244070", "/Drive", null, false)] + [InlineData("/Drive/Drive.S01.2160p.WEB-DL.DDP5.1.H.265-XXXX", "/Drive", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.1080p.PDTV.x264-JCH", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/[The.Wonder.Years.S04.1080p.PDTV.x264-JCH]", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/The.Wonder.Years [S04][1080p.PDTV.x264-JCH]", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/The Wonder Years Season 01 1080p", "/The Wonder Years", 1, true)] + public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, parentPath, true, true); From 74c9629372b9347f9e53fcb549488d2df4c969b9 Mon Sep 17 00:00:00 2001 From: JPVenson <github@jpb.email> Date: Mon, 17 Nov 2025 14:08:55 -0500 Subject: [PATCH 087/300] Backport pull request #15413 from jellyfin/release-10.11.z Fixed missing sort argument Original-merge: 91c3b1617e06283c88f36bc63046b99c993cb774 Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- .../Item/BaseItemRepository.cs | 17 +++-- .../Item/OrderMapper.cs | 72 +++++++++++-------- .../Item/OrderMapperTests.cs | 2 +- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 06fb8ec217..060705f746 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -275,6 +275,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.StartIndex = filter.StartIndex ?? 0; @@ -294,6 +295,7 @@ public sealed class BaseItemRepository dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); + dbQuery = ApplyNavigations(dbQuery, filter); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); } @@ -337,6 +339,8 @@ public sealed class BaseItemRepository mainquery = ApplyGroupingFilter(context, mainquery, filter); mainquery = ApplyQueryPaging(mainquery, filter); + mainquery = ApplyNavigations(mainquery, filter); + return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); } @@ -399,9 +403,7 @@ public sealed class BaseItemRepository dbQuery = dbQuery.Distinct(); } - dbQuery = ApplyOrder(dbQuery, filter); - - dbQuery = ApplyNavigations(dbQuery, filter); + dbQuery = ApplyOrder(dbQuery, filter, context); return dbQuery; } @@ -446,6 +448,7 @@ public sealed class BaseItemRepository dbQuery = TranslateQuery(dbQuery, context, filter); dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); + dbQuery = ApplyNavigations(dbQuery, filter); return dbQuery; } @@ -1252,7 +1255,7 @@ public sealed class BaseItemRepository .AsSingleQuery() .Where(e => masterQuery.Contains(e.Id)); - query = ApplyOrder(query, filter); + query = ApplyOrder(query, filter, context); var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); if (filter.EnableTotalRecordCount) @@ -1518,7 +1521,7 @@ public sealed class BaseItemRepository || query.IncludeItemTypes.Contains(BaseItemKind.Season); } - private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter) + private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context) { var orderBy = filter.OrderBy; var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); @@ -1537,7 +1540,7 @@ public sealed class BaseItemRepository var firstOrdering = orderBy.FirstOrDefault(); if (firstOrdering != default) { - var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter); + var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context); if (firstOrdering.SortOrder == SortOrder.Ascending) { orderedQuery = query.OrderBy(expression); @@ -1562,7 +1565,7 @@ public sealed class BaseItemRepository foreach (var item in orderBy.Skip(1)) { - var expression = OrderMapper.MapOrderByField(item.OrderBy, filter); + var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context); if (item.SortOrder == SortOrder.Ascending) { orderedQuery = orderedQuery!.ThenBy(expression); diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index a0c1270311..192ee74996 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -1,7 +1,10 @@ +#pragma warning disable RS0030 // Do not use banned APIs + using System; using System.Linq; using System.Linq.Expressions; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using Microsoft.EntityFrameworkCore; @@ -18,39 +21,50 @@ public static class OrderMapper /// </summary> /// <param name="sortBy">Item property to sort by.</param> /// <param name="query">Context Query.</param> + /// <param name="jellyfinDbContext">Context.</param> /// <returns>Func to be executed later for sorting query.</returns> - public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + public static Expression<Func<BaseItemEntity, object?>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext) { - return sortBy switch + return (sortBy, query.User) switch { - ItemSortBy.AirTime => e => e.SortName, // TODO - ItemSortBy.Runtime => e => e.RunTimeTicks, - ItemSortBy.Random => e => EF.Functions.Random(), - ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, - ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, - ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, - ItemSortBy.IsFolder => e => e.IsFolder, - ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, - ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, - ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, - ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => e => e.SeriesName, + (ItemSortBy.AirTime, _) => e => e.SortName, // TODO + (ItemSortBy.Runtime, _) => e => e.RunTimeTicks, + (ItemSortBy.Random, _) => e => EF.Functions.Random(), + (ItemSortBy.DatePlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, + (ItemSortBy.PlayCount, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, + (ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, + (ItemSortBy.IsFolder, _) => e => e.IsFolder, + (ItemSortBy.IsPlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, + (ItemSortBy.IsUnplayed, _) => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, + (ItemSortBy.DateLastContentAdded, _) => e => e.DateLastMediaAdded, + (ItemSortBy.Artist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + (ItemSortBy.AlbumArtist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + (ItemSortBy.Studio, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + (ItemSortBy.OfficialRating, _) => e => e.InheritedParentalRatingValue, + (ItemSortBy.SeriesSortName, _) => e => e.SeriesName, + (ItemSortBy.Album, _) => e => e.Album, + (ItemSortBy.DateCreated, _) => e => e.DateCreated, + (ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), + (ItemSortBy.StartDate, _) => e => e.StartDate, + (ItemSortBy.Name, _) => e => e.CleanName, + (ItemSortBy.CommunityRating, _) => e => e.CommunityRating, + (ItemSortBy.ProductionYear, _) => e => e.ProductionYear, + (ItemSortBy.CriticRating, _) => e => e.CriticRating, + (ItemSortBy.VideoBitRate, _) => e => e.TotalBitrate, + (ItemSortBy.ParentIndexNumber, _) => e => e.ParentIndexNumber, + (ItemSortBy.IndexNumber, _) => e => e.IndexNumber, + (ItemSortBy.SeriesDatePlayed, not null) => e => + jellyfinDbContext.BaseItems + .Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey) + .Join(jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id && w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate) + .Max(f => f), + (ItemSortBy.SeriesDatePlayed, null) => e => jellyfinDbContext.BaseItems.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey) + .Join(jellyfinDbContext.UserData.Where(w => w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate) + .Max(f => f), + // ItemSortBy.SeriesDatePlayed => e => jellyfinDbContext.UserData + // .Where(u => u.Item!.SeriesPresentationUniqueKey == e.PresentationUniqueKey && u.Played) + // .Max(f => f.LastPlayedDate), // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => e => e.Album, - ItemSortBy.DateCreated => e => e.DateCreated, - ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), - ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.CleanName, - ItemSortBy.CommunityRating => e => e.CommunityRating, - ItemSortBy.ProductionYear => e => e.ProductionYear, - ItemSortBy.CriticRating => e => e.CriticRating, - ItemSortBy.VideoBitRate => e => e.TotalBitrate, - ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, - ItemSortBy.IndexNumber => e => e.IndexNumber, _ => e => e.SortName }; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs index caf2b06b73..8ac3e5e317 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs @@ -12,7 +12,7 @@ public class OrderMapperTests [Fact] public void ShouldReturnMappedOrderForSortingByPremierDate() { - var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery()).Compile(); + var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery(), null!).Compile(); var expectedDate = new DateTime(1, 2, 3); var expectedProductionYearDate = new DateTime(4, 1, 1); From 5a30f108fe535d7ab700826036ce319ebf367e32 Mon Sep 17 00:00:00 2001 From: nielsvanvelzen <nielsvanvelzen@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:56 -0500 Subject: [PATCH 088/300] Backport pull request #15422 from jellyfin/release-10.11.z Update branding in Swagger page Original-merge: d1406302085148aea4d3b03dab78c4d49909c4fe Merged-by: crobibero <cody@robibe.ro> Backported-by: Bond_009 <bond.009@outlook.com> --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- .../wwwroot/api-docs/banner-dark.svg | 34 ------------------- Jellyfin.Server/wwwroot/api-docs/jellyfin.svg | 26 ++++++++++++++ .../wwwroot/api-docs/swagger/custom.css | 12 ++++--- 4 files changed, 34 insertions(+), 40 deletions(-) delete mode 100644 Jellyfin.Server/wwwroot/api-docs/banner-dark.svg create mode 100644 Jellyfin.Server/wwwroot/api-docs/jellyfin.svg diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index df630922a0..14ab114fb4 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -78,7 +78,7 @@ <None Update="wwwroot\api-docs\swagger\custom.css"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> - <None Update="wwwroot\api-docs\banner-dark.svg"> + <None Update="wwwroot\api-docs\jellyfin.svg"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Update="ServerSetupApp/index.mstemplate.html"> diff --git a/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg b/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg deleted file mode 100644 index b62b7545c7..0000000000 --- a/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- ***** BEGIN LICENSE BLOCK ***** - - Part of the Jellyfin project (https://jellyfin.media) - - - - All copyright belongs to the Jellyfin contributors; a full list can - - be found in the file CONTRIBUTORS.md - - - - This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. - - To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/. -- ***** END LICENSE BLOCK ***** --> -<svg id="banner-dark" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1536 512"> - <defs> - <linearGradient id="linear-gradient" x1="110.25" y1="213.3" x2="496.14" y2="436.09" gradientUnits="userSpaceOnUse"> - <stop offset="0" stop-color="#aa5cc3"/> - <stop offset="1" stop-color="#00a4dc"/> - </linearGradient> - </defs> - <title>banner-dark - - \ No newline at end of file diff --git a/Jellyfin.Server/wwwroot/api-docs/jellyfin.svg b/Jellyfin.Server/wwwroot/api-docs/jellyfin.svg new file mode 100644 index 0000000000..692530319b --- /dev/null +++ b/Jellyfin.Server/wwwroot/api-docs/jellyfin.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css index acb59888e0..c14ad60215 100644 --- a/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css +++ b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css @@ -4,12 +4,14 @@ } .topbar-wrapper .link:after { - content: url(../banner-dark.svg); + content: ''; display: block; - -moz-box-sizing: border-box; + background-image: url(../jellyfin.svg); + background-position: center; + background-repeat: no-repeat; + background-size: contain; box-sizing: border-box; - max-width: 100%; - max-height: 100%; - width: 150px; + width: 220px; + height: 40px; } /* end logo */ From e34e7a1d0b9d1a1ed121fcf4feea441571a5c13b Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:57 -0500 Subject: [PATCH 089/300] Backport pull request #15423 from jellyfin/release-10.11.z Invalidate parent folder's cache on deletion/creation Original-merge: 49efd68fc7ef4b70b38151a177502bbdb3adede0 Merged-by: crobibero Backported-by: Bond_009 --- .../Library/LibraryManager.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a400cb0925..cab87e53de 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -457,6 +457,12 @@ namespace Emby.Server.Implementations.Library _cache.TryRemove(child.Id, out _); } + if (parent is Folder folder) + { + folder.Children = null; + folder.UserData = null; + } + ReportItemRemoved(item, parent); } @@ -1993,6 +1999,12 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } + if (parent is Folder folder) + { + folder.Children = null; + folder.UserData = null; + } + if (ItemAdded is not null) { foreach (var item in items) @@ -2150,6 +2162,12 @@ namespace Emby.Server.Implementations.Library _itemRepository.SaveItems(items, cancellationToken); + if (parent is Folder folder) + { + folder.Children = null; + folder.UserData = null; + } + if (ItemUpdated is not null) { foreach (var item in items) From de9e653b7361557faac44cf7ffed58a6b2bfcaa1 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:58 -0500 Subject: [PATCH 090/300] Backport pull request #15435 from jellyfin/release-10.11.z Fix search terms using diacritics Original-merge: 63a3e552978010e905ea9b4e258c0c2e153ddfec Merged-by: crobibero Backported-by: Bond_009 --- .../Item/BaseItemRepository.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 060705f746..7117e0a8d7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1704,15 +1704,16 @@ public sealed class BaseItemRepository if (!string.IsNullOrEmpty(filter.SearchTerm)) { - var searchTerm = filter.SearchTerm.ToLower(); - if (SearchWildcardTerms.Any(f => searchTerm.Contains(f))) + var cleanedSearchTerm = GetCleanValue(filter.SearchTerm); + var originalSearchTerm = filter.SearchTerm.ToLower(); + if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f))) { - searchTerm = $"%{searchTerm.Trim('%')}%"; - baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), searchTerm))); + cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%"; + baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm))); } else { - baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm))); + baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm))); } } From 26ac2ccd742d4b0b0edc9a806b023089c51930b2 Mon Sep 17 00:00:00 2001 From: IceStormNG Date: Mon, 17 Nov 2025 14:08:59 -0500 Subject: [PATCH 091/300] Backport pull request #15441 from jellyfin/release-10.11.z Fix System.NullReferenceException when people's role is null (10.11.z) Original-merge: 5a9a8363f45ed34b2db1f8f59c2be69ff458f8ab Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index bdb6b93beb..bde23e842f 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -520,7 +520,7 @@ namespace MediaBrowser.Providers.MediaInfo { Name = person.Name, Type = person.Type, - Role = person.Role.Trim() + Role = person.Role?.Trim() }); } } From 2e7d7752e931631a9acaf02f839a4106ffd94e35 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:01 -0500 Subject: [PATCH 092/300] Backport pull request #15446 from jellyfin/release-10.11.z Fix AncestorIds not migrating Original-merge: 177b6464ca1b6772a191dbf9c5595708f91fc0fa Merged-by: crobibero Backported-by: Bond_009 --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index b90da9f7d3..d221d18531 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -383,8 +383,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine }); } - baseItemIds.Clear(); - foreach (var item in peopleCache) { operation.JellyfinDbContext.Peoples.Add(item.Value.Person); From e51680cf56f423c0401dad87e030be48337af5cb Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:02 -0500 Subject: [PATCH 093/300] Backport pull request #15462 from jellyfin/release-10.11.z Fix NullReferenceException in GetPathProtocol when path is null Original-merge: 7c1063177f5647e07c634d36694f0bdec8fe4ff1 Merged-by: joshuaboniface Backported-by: Bond_009 --- Emby.Server.Implementations/Library/MediaSourceManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 750346169f..c667fb0600 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.Library /// > public MediaProtocol GetPathProtocol(string path) { + if (string.IsNullOrEmpty(path)) + { + return MediaProtocol.File; + } + if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Rtsp; From d7f628677e45cca6a4c34b57ff37a4d81e19277f Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 17 Nov 2025 14:09:03 -0500 Subject: [PATCH 094/300] Backport pull request #15466 from jellyfin/release-10.11.z Don't error out when searching for marker files fails Original-merge: f4a846aa4dcffb3be7b701f806b24cb8dd6b7c5d Merged-by: crobibero Backported-by: Bond_009 --- .../AppBase/BaseApplicationPaths.cs | 14 ++++++++++++-- .../Configuration/IApplicationPaths.cs | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index c69bcfef78..de722332a4 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -107,10 +107,20 @@ namespace Emby.Server.Implementations.AppBase private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) { - var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); + string? otherMarkers = null; + try + { + otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase)); + } + catch + { + // Error while checking for marker files, assume none exist and keep going + // TODO: add some logging + } + if (otherMarkers is not null) { - throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); + throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}."); } var markerPath = Path.Combine(path, markerName); diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 6d1a72b042..3a61974901 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -103,11 +103,11 @@ namespace MediaBrowser.Common.Configuration void MakeSanityCheckOrThrow(); /// - /// Checks and creates the given path and adds it with a marker file if non existant. + /// Checks and creates the given path and adds it with a marker file if non existent. /// /// The path to check. /// The common marker file name. - /// Check for other settings paths recursivly. + /// Check for other settings paths recursively. void CreateAndCheckMarker(string path, string markerName, bool recursive = false); } } From 99c68ddd50353b62f86067a8336c001f0592131d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 17 Nov 2025 14:09:05 -0500 Subject: [PATCH 095/300] Backport pull request #15468 from jellyfin/release-10.11.z Check if target exists before trying to follow it Original-merge: 5878b1ffc569bc7e7204a07e77e0bb57d3984e56 Merged-by: joshuaboniface Backported-by: Bond_009 --- MediaBrowser.Controller/IO/FileSystemHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 324aea7e3b..3e390ca428 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -92,6 +92,11 @@ public static class FileSystemHelper return null; } + if (!targetInfo.Exists) + { + return targetInfo; + } + var currentPath = targetInfo.FullName; var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) From 4e68a5a078a7f1b5daf382354b83ae7a2657d1cb Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:06 -0500 Subject: [PATCH 096/300] Backport pull request #15472 from jellyfin/release-10.11.z Fix series DateLastMediaAdded not updating when new episodes are added Original-merge: abfbaca33686ab5214182992ab644a6e24d4b180 Merged-by: crobibero Backported-by: Bond_009 --- .../Manager/MetadataService.cs | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 4c83845992..21767af84f 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -317,12 +317,8 @@ namespace MediaBrowser.Providers.Manager { if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType)) { - if (isFullRefresh || updateType > ItemUpdateType.None) - { - var children = GetChildrenForMetadataUpdates(item); - - updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType); - } + var children = GetChildrenForMetadataUpdates(item); + updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType); } var presentationUniqueKey = item.CreatePresentationUniqueKey(); @@ -362,16 +358,24 @@ namespace MediaBrowser.Providers.Manager protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType) { - if (isFullRefresh || currentUpdateType > ItemUpdateType.None) + if (item is Folder folder) { - if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren) + if (!isFullRefresh && currentUpdateType == ItemUpdateType.None) { - return true; + return folder.SupportsDateLastMediaAdded; } - if (item is Folder folder) + if (isFullRefresh || currentUpdateType > ItemUpdateType.None) { - return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks; + if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren) + { + return true; + } + + if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks) + { + return true; + } } } @@ -392,36 +396,42 @@ namespace MediaBrowser.Providers.Manager { var updateType = ItemUpdateType.None; - if (isFullRefresh || currentUpdateType > ItemUpdateType.None) + if (item is Folder folder) { - updateType |= UpdateCumulativeRunTimeTicks(item, children); - updateType |= UpdateDateLastMediaAdded(item, children); - - // don't update user-changeable metadata for locked items - if (item.IsLocked) + if (folder.SupportsDateLastMediaAdded) { - return updateType; + updateType |= UpdateDateLastMediaAdded(item, children); } - if (EnableUpdatingPremiereDateFromChildren) + if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks) { - updateType |= UpdatePremiereDate(item, children); + updateType |= UpdateCumulativeRunTimeTicks(item, children); } + } - if (EnableUpdatingGenresFromChildren) - { - updateType |= UpdateGenres(item, children); - } + if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked) + { + return updateType; + } - if (EnableUpdatingStudiosFromChildren) - { - updateType |= UpdateStudios(item, children); - } + if (EnableUpdatingPremiereDateFromChildren) + { + updateType |= UpdatePremiereDate(item, children); + } - if (EnableUpdatingOfficialRatingFromChildren) - { - updateType |= UpdateOfficialRating(item, children); - } + if (EnableUpdatingGenresFromChildren) + { + updateType |= UpdateGenres(item, children); + } + + if (EnableUpdatingStudiosFromChildren) + { + updateType |= UpdateStudios(item, children); + } + + if (EnableUpdatingOfficialRatingFromChildren) + { + updateType |= UpdateOfficialRating(item, children); } return updateType; From a08b6ac2664003cb89804ce192f9ae3f3acb4be9 Mon Sep 17 00:00:00 2001 From: CBPJ Date: Mon, 17 Nov 2025 14:09:07 -0500 Subject: [PATCH 097/300] Backport pull request #15487 from jellyfin/release-10.11.z Fix gitignore-style not working properly on windows. Original-merge: 435bb14bb266916e9c6f100c4324a94c36126e06 Merged-by: crobibero Backported-by: Bond_009 --- .../Library/DotIgnoreIgnoreRule.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index e53502046a..46e60dbaa4 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.Runtime.InteropServices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -88,6 +89,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule var ignore = new Ignore.Ignore(); ignore.Add(ignoreRules); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Mitigate the problem of the Ignore library not handling Windows paths correctly. + // See https://github.com/jellyfin/jellyfin/issues/15484 + return ignore.IsIgnored(fileInfo.FullName.NormalizePath('/')); + } + return ignore.IsIgnored(fileInfo.FullName); } From c869b5b884d608aa38d597fa14fcdf02423b2053 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:08 -0500 Subject: [PATCH 098/300] Backport pull request #15493 from jellyfin/release-10.11.z Remove InheritedTags and update tag filtering logic Original-merge: 4b38e35bbbb65c77f251288ff64ee28da4a48943 Merged-by: crobibero Backported-by: Bond_009 --- .../Item/BaseItemRepository.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 7117e0a8d7..2c18ce69ac 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2420,39 +2420,34 @@ public sealed class BaseItemRepository if (filter.ExcludeInheritedTags.Length > 0) { - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))); + 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)))); } if (filter.IncludeInheritedTags.Length > 0) { - // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. - // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. - if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) + // 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!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || - (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)))); + 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)))); } // 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!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); + 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!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); + baseQuery = baseQuery.Where(e => + e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); } } From 5b3f29946b5628b975578c1ef4a15a3248cd650a Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:09 -0500 Subject: [PATCH 099/300] Backport pull request #15501 from jellyfin/release-10.11.z Fix .ignore handling for directories Original-merge: e8150428b62668e062a3432960f98684d3b352cb Merged-by: crobibero Backported-by: Bond_009 --- .../Library/DotIgnoreIgnoreRule.cs | 103 +++++++----------- 1 file changed, 42 insertions(+), 61 deletions(-) diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 46e60dbaa4..473ff8e1d7 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Runtime.InteropServices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -13,28 +12,24 @@ namespace Emby.Server.Implementations.Library; /// public class DotIgnoreIgnoreRule : IResolverIgnoreRule { + private static readonly bool IsWindows = OperatingSystem.IsWindows(); + private static FileInfo? FindIgnoreFile(DirectoryInfo directory) { - var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); - if (ignoreFile.Exists) + for (var current = directory; current is not null; current = current.Parent) { - return ignoreFile; + var ignorePath = Path.Join(current.FullName, ".ignore"); + if (File.Exists(ignorePath)) + { + return new FileInfo(ignorePath); + } } - var parentDir = directory.Parent; - if (parentDir is null) - { - return null; - } - - return FindIgnoreFile(parentDir); + return null; } /// - public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) - { - return IsIgnored(fileInfo, parent); - } + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent); /// /// Checks whether or not the file is ignored. @@ -44,72 +39,58 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule /// True if the file should be ignored. public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) { - if (fileInfo.IsDirectory) - { - var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); - if (dirIgnoreFile is null) - { - return false; - } + var searchDirectory = fileInfo.IsDirectory + ? new DirectoryInfo(fileInfo.FullName) + : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty); - // Fast path in case the ignore files isn't a symlink and is empty - if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0) - { - return true; - } - - // ignore the directory only if the .ignore file is empty - // evaluate individual files otherwise - return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); - } - - var parentDirPath = Path.GetDirectoryName(fileInfo.FullName); - if (string.IsNullOrEmpty(parentDirPath)) + if (string.IsNullOrEmpty(searchDirectory.FullName)) { return false; } - var folder = new DirectoryInfo(parentDirPath); - var ignoreFile = FindIgnoreFile(folder); + var ignoreFile = FindIgnoreFile(searchDirectory); if (ignoreFile is null) { return false; } - string ignoreFileString = GetFileContent(ignoreFile); - - if (string.IsNullOrWhiteSpace(ignoreFileString)) + // Fast path in case the ignore files isn't a symlink and is empty + if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0) { // Ignore directory if we just have the file return true; } - // If file has content, base ignoring off the content .gitignore-style rules - var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); - var ignore = new Ignore.Ignore(); - ignore.Add(ignoreRules); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Mitigate the problem of the Ignore library not handling Windows paths correctly. - // See https://github.com/jellyfin/jellyfin/issues/15484 - return ignore.IsIgnored(fileInfo.FullName.NormalizePath('/')); - } - - return ignore.IsIgnored(fileInfo.FullName); + var content = GetFileContent(ignoreFile); + return string.IsNullOrWhiteSpace(content) + || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory); } - private static string GetFileContent(FileInfo dirIgnoreFile) + private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory) { - dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; - if (!dirIgnoreFile.Exists) + // If file has content, base ignoring off the content .gitignore-style rules + var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var ignore = new Ignore.Ignore(); + ignore.Add(rules); + + // 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; + + // Add trailing slash for directories to match "folder/" + if (isDirectory) { - return string.Empty; + pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/"); } - using (var reader = dirIgnoreFile.OpenText()) - { - return reader.ReadToEnd(); - } + return ignore.IsIgnored(pathToCheck); + } + + private static string GetFileContent(FileInfo ignoreFile) + { + ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile; + return ignoreFile.Exists + ? File.ReadAllText(ignoreFile.FullName) + : string.Empty; } } From 8fa36a38e263a8e90711dbf9dc987ab0835d33bc Mon Sep 17 00:00:00 2001 From: Iksas Date: Mon, 17 Nov 2025 14:09:10 -0500 Subject: [PATCH 100/300] Backport pull request #15502 from jellyfin/release-10.11.z Fix font extraction for certain transcoding settings Original-merge: ee34c75386cc1a0ca0e15196a43b685fa0e73130 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 0cda803d64..2fd054f110 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -396,7 +396,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath); // If subtitles get burned in fonts may need to be extracted from the media file - if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (state.SubtitleStream is not null && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode || state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding)) { if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { From 7e25089c0868d780a8285028590c7e866fe7895b Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:12 -0500 Subject: [PATCH 101/300] Backport pull request #15508 from jellyfin/release-10.11.z Fix playlist DateCreated and DateLastMediaAdded not being set Original-merge: 078f9584ed3622eed3516488026cbb6e42242bba Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 1 + MediaBrowser.Providers/Manager/MetadataService.cs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index c9d76df0bf..1577c5c9c7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -244,6 +244,7 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; + playlist.DateLastMediaAdded = DateTime.UtcNow; await UpdatePlaylistInternal(playlist).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 21767af84f..c8435aa681 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -340,7 +340,10 @@ namespace MediaBrowser.Providers.Manager item.DateModified = info.LastWriteTimeUtc; if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded) { - item.DateCreated = info.CreationTimeUtc; + if (info.CreationTimeUtc > DateTime.MinValue) + { + item.DateCreated = info.CreationTimeUtc; + } } if (item is Video video) From faf1cea63e042e3b2bf4ab87f38d86d5a2de0b07 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:13 -0500 Subject: [PATCH 102/300] Backport pull request #15514 from jellyfin/release-10.11.z Add 1 minute tolerance for NFO change detection Original-merge: 6566188e453b42604dbb3ce532937951e88565d0 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index c671e7a932..5ac672f105 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -68,12 +68,15 @@ namespace MediaBrowser.XbmcMetadata.Providers { var file = GetXmlFile(new ItemInfo(item), directoryService); - if (file is null) + if (file?.Exists is not true) { return false; } - return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; + var fileTime = _fileSystem.GetLastWriteTimeUtc(file); + + // 1 minute tolerance to avoid detecting our own file writes + return (fileTime - item.DateLastSaved) > TimeSpan.FromMinutes(1); } protected abstract void Fetch(MetadataResult result, string path, CancellationToken cancellationToken); From 2af43e01310b5fe725b3ea88310e911b34e9e9b5 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:14 -0500 Subject: [PATCH 103/300] Backport pull request #15529 from jellyfin/release-10.11.z Fix movie titles using folder name when NFO saver is enabled Original-merge: f8e012582a8819d18ad933fd65eade936bdc946d Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Providers/Manager/MetadataService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index c8435aa681..f220ec4a14 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -151,7 +151,10 @@ namespace MediaBrowser.Providers.Manager .ConfigureAwait(false); updateType |= beforeSaveResult; - updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + if (!isFirstRefresh) + { + updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + } // Next run metadata providers if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) From 55dbff8f30a26f44d5099601cee016d58b90307f Mon Sep 17 00:00:00 2001 From: Rufis72 Date: Mon, 17 Nov 2025 23:43:06 -0500 Subject: [PATCH 104/300] Translated using Weblate (English (Pirate)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/en@pirate/ --- Emby.Server.Implementations/Localization/Core/pr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 7b1cd886f0..fee7e65f1d 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -114,7 +114,7 @@ "TaskRefreshChapterImages": "Claim chapter portraits", "TaskRefreshChapterImagesDescription": "Paints wee portraits fer videos that own chapters.", "TaskRefreshLibrary": "Scan the Treasure Trove", - "TasksChannelsCategory": "Channels of the Internet", + "TasksChannelsCategory": "Channels o' thy Internet", "TaskRefreshTrickplayImages": "Summon the picture tricks", "TaskRefreshTrickplayImagesDescription": "Summons picture trick previews for videos in ye enabled book roost", "TaskUpdatePlugins": "Resummon yer Plugins", From 196c243a7d53d06a5fe492942442d48bba9727c4 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 18 Nov 2025 16:13:48 +0100 Subject: [PATCH 105/300] 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; + +/// +/// Migration to disable legacy authorization in the system config. +/// +#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; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public DisableLegacyAuthorization(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// + 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 /// /// Gets or sets a value indicating whether old authorization methods are allowed. /// - public bool EnableLegacyAuthorization { get; set; } = true; + public bool EnableLegacyAuthorization { get; set; } } From 8ac97f5471ea762e86823f92673789672c13906a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 01:37:24 +0000 Subject: [PATCH 106/300] Update github/codeql-action action to v4.31.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 cbab3efea8..dac9b9af1e 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@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 From aa4ddd139a7c01889a99561fc314121ba198dd70 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Tue, 18 Nov 2025 21:05:43 -0500 Subject: [PATCH 107/300] Add all 10.11 versions to issue template (#15565) --- .github/ISSUE_TEMPLATE/issue report.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index d304fb7291..2ed8b05122 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,7 +87,10 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: - - 10.11.0+ + - 10.11.3 + - 10.11.2 + - 10.11.1 + - 10.11.0 - Master - Unstable - Older* From 569f8cfcfc33aba5d14f0155bb1ad896d6b0fbac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:58:53 +0000 Subject: [PATCH 108/300] Update actions/checkout action to v6 --- .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 | 4 ++-- .github/workflows/issue-template-check.yml | 2 +- .github/workflows/release-bump-version.yaml | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index cbab3efea8..744912dafe 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: Setup .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index d78af466a0..d483e048f7 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 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 10cc386865..a957357f3d 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 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 0f47e8ca83..d2d649f165 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 13fc61d21b..729bcf69bb 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: jellyfin/jellyfin-triage-script - name: install python diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index fb868be4ce..1af139054a 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: repository: jellyfin/jellyfin-triage-script - name: install python diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml index 943cc3f4fc..fa4025de50 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: ref: ${{ env.TAG_BRANCH }} From 67c67df5077af5c90242fc5fc36373b2ae6f2c91 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Thu, 20 Nov 2025 22:11:55 +0100 Subject: [PATCH 109/300] 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; /// /// Migration to disable legacy authorization in the system config. /// -#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 } /// - 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 Date: Sat, 22 Nov 2025 16:25:59 +0900 Subject: [PATCH 110/300] 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 +{ + /// + /// Provides the primary image for EPUB items that have embedded covers. + /// + public class EpubImageProvider : IDynamicImageProvider + { + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public EpubImageProvider(ILogger logger) + { + _logger = logger; + } + + /// + public string Name => "EPUB Metadata"; + + /// + public bool Supports(BaseItem item) + { + return item is Book; + } + + /// + public IEnumerable GetSupportedImages(BaseItem item) + { + yield return ImageType.Primary; + } + + /// + public Task 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 LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory) + { + var utilities = new OpfReader(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 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 +{ + /// + /// Provides book metadata from OPF content in an EPUB item. + /// + public class EpubProvider : ILocalMetadataProvider + { + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public EpubProvider(IFileSystem fileSystem, ILogger logger) + { + _fileSystem = fileSystem; + _logger = logger; + } + + /// + public string Name => "EPUB Metadata"; + + /// + public Task> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken) + { + var path = GetEpubFile(info.Path)?.FullName; + + if (path is null) + { + return Task.FromResult(new MetadataResult { HasMetadata = false }); + } + + var result = ReadEpubAsZip(path, cancellationToken); + + if (result is null) + { + return Task.FromResult(new MetadataResult { 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? 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(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 +{ + /// + /// Utilities for EPUB files. + /// + public static class EpubUtils + { + private const string ContainerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container"; + + /// + /// Attempt to read content from ZIP archive. + /// + /// The ZIP archive. + /// The content file path. + 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 +{ + /// + /// 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. + /// + public class OpfProvider : ILocalMetadataProvider, IHasItemChangeMonitor + { + private const string StandardOpfFile = "content.opf"; + private const string CalibreOpfFile = "metadata.opf"; + + private readonly IFileSystem _fileSystem; + + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public OpfProvider(IFileSystem fileSystem, ILogger logger) + { + _fileSystem = fileSystem; + _logger = logger; + } + + /// + public string Name => "Open Packaging Format"; + + /// + public bool HasChanged(BaseItem item, IDirectoryService directoryService) + { + var file = GetXmlFile(item.Path); + + return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; + } + + /// + public Task> 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 { 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 ReadOpfData(string file, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var doc = new XmlDocument(); + doc.Load(file); + + var utilities = new OpfReader(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 +{ + /// + /// Methods used to pull metadata and other information from Open Packaging Format in XML objects. + /// + /// The type of category. + public class OpfReader + { + 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 _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The XML document to parse. + /// Instance of the interface. + public OpfReader(XmlDocument document, ILogger logger) + { + _document = document; + _logger = logger; + _namespaceManager = new XmlNamespaceManager(_document.NameTable); + + _namespaceManager.AddNamespace("dc", DcNamespace); + _namespaceManager.AddNamespace("opf", OpfNamespace); + } + + /// + /// Checks for the existence of a cover image. + /// + /// The root directory in which the OPF file is located. + /// Returns the found cover and its type or null. + 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; + } + + /// + /// Read all supported OPF data from the file. + /// + /// The cancellation token. + /// The metadata result to update. + public MetadataResult ReadOpfData(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var book = CreateBookFromOpf(); + var result = new MetadataResult { 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().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) + { + 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 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 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 111/300] 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 @@ - + From c08e81c52b7787deab799cb7ac96cfdedc2818b7 Mon Sep 17 00:00:00 2001 From: rimasx Date: Sun, 23 Nov 2025 00:57:01 -0500 Subject: [PATCH 112/300] 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 Date: Sun, 23 Nov 2025 21:37:56 +0900 Subject: [PATCH 113/300] 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 /// public static class EpubUtils { - private const string ContainerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container"; - /// /// Attempt to read content from ZIP archive. /// @@ -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 Date: Sun, 23 Nov 2025 21:40:52 +0900 Subject: [PATCH 114/300] 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().Where(node => !book.Genres.Contains(node.InnerText))) + foreach (var node in genreNodes.Cast().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 115/300] 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 @@ - + 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 116/300] 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 117/300] 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?= Date: Fri, 28 Nov 2025 15:01:20 -0500 Subject: [PATCH 118/300] 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 119/300] 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 Date: Sun, 30 Nov 2025 15:22:54 +0100 Subject: [PATCH 120/300] 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 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 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(); 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(); - 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(resultArray); } @@ -747,8 +747,7 @@ public class LibraryController : BaseJellyfinApiController return new QueryResult(); } - 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(); 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 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 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 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 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(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; /// public static class DtoExtensions { - /// - /// Add additional fields depending on client. - /// - /// - /// Use in place of GetDtoOptions. - /// Legacy order: 2. - /// - /// DtoOptions object. - /// Current claims principal. - /// Modified DtoOptions object. - 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; - } - /// /// Add additional DtoOptions. /// From 691c194152df841e4ebd753b1c3a0d75e5d13e79 Mon Sep 17 00:00:00 2001 From: Hasan Abdulaal Date: Tue, 2 Dec 2025 06:11:42 -0500 Subject: [PATCH 121/300] 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 122/300] 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 Date: Wed, 3 Dec 2025 09:45:50 -0500 Subject: [PATCH 123/300] 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 124/300] Backport pull request #15548 from jellyfin/release-10.11.z Fix NullReferenceException in filesystem path comparison Original-merge: 5ae444d96d473ba42c4a812c3f366b0faa6ebef4 Merged-by: crobibero Backported-by: Bond_009 --- 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 /// 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 Date: Wed, 3 Dec 2025 14:04:18 -0500 Subject: [PATCH 125/300] 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 Backported-by: Bond_009 --- 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 Date: Wed, 3 Dec 2025 14:04:19 -0500 Subject: [PATCH 126/300] Backport pull request #15557 from jellyfin/release-10.11.z Restrict first video frame probing to file protocol Original-merge: ee7ad83427ed30aa095896ff3577bb946f3d1c02 Merged-by: crobibero Backported-by: Bond_009 --- 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 127/300] 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 Backported-by: Bond_009 --- 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 128/300] Backport pull request #15564 from jellyfin/release-10.11.z Fix locked fields not saving Original-merge: 0ee81e87be58072e21a3bc69fc1d1d0fbc83974a Merged-by: crobibero Backported-by: Bond_009 --- 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 129/300] Backport pull request #15568 from jellyfin/release-10.11.z Fix ResolveLinkTarget crashing on exFAT drives Original-merge: fbb9a0b2c7c5afbc56be76a4eb11a1045f0ab0f0 Merged-by: crobibero Backported-by: Bond_009 --- .../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 } } + /// + /// Resolves a single link hop for the specified path. + /// + /// + /// Returns null if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT). + /// + /// The file path to resolve. + /// + /// A representing the next link target if the path is a link; otherwise, null. + /// + 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; + } + } + /// /// Gets the target of the specified file link. /// @@ -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(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 Date: Wed, 3 Dec 2025 14:04:24 -0500 Subject: [PATCH 130/300] Backport pull request #15582 from jellyfin/release-10.11.z Add hidden file check in BdInfoDirectoryInfo.cs. Original-merge: 29b3aa854310c150e23ec27a41d912fd6fde3c7d Merged-by: crobibero Backported-by: Bond_009 --- 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 name) => name.StartsWith('.'); + /// /// Gets the directories. /// @@ -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 131/300] Backport pull request #15594 from jellyfin/release-10.11.z Fix isMovie filter logic Original-merge: 94f3725208caa030910b62b798ad2f78608d6fd6 Merged-by: crobibero Backported-by: Bond_009 --- .../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 Date: Wed, 3 Dec 2025 14:04:27 -0500 Subject: [PATCH 132/300] Backport pull request #15670 from jellyfin/release-10.11.z Fix the empty output of trickplay on RK3576 Original-merge: 98d1d0cb35a56eadfde335916a937940faf75a23 Merged-by: nielsvanvelzen Backported-by: Bond_009 --- .../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 Date: Wed, 3 Dec 2025 14:04:28 -0500 Subject: [PATCH 133/300] Backport pull request #15672 from jellyfin/release-10.11.z Cache OpenApi document generation Original-merge: 8cd56521570992d8587db5e1f80d4cb826537f31 Merged-by: anthonylavado Backported-by: Bond_009 --- .../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(); c.OperationFilter(); c.DocumentFilter(); - }); + }) + .Replace(ServiceDescriptor.Transient()); } 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; + +/// +/// OpenApi provider with caching. +/// +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; + + /// + /// Initializes a new instance of the class. + /// + /// The options accessor. + /// The api descriptions provider. + /// The schema generator. + /// The memory cache. + public CachingOpenApiProvider( + IOptions optionsAccessor, + IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + ISchemaGenerator schemaGenerator, + IMemoryCache memoryCache) + { + _swaggerGeneratorOptions = optionsAccessor.Value; + _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); + _memoryCache = memoryCache; + } + + /// + 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 Date: Thu, 4 Dec 2025 09:41:47 -0500 Subject: [PATCH 134/300] 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 135/300] 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 136/300] 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) + /// + /// Gets the clean value for search and sorting purposes. + /// + /// The value to clean. + /// The cleaned value. + 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 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; + +/// +/// Migration to refresh CleanName values for all library items. +/// +[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))] +[JellyfinMigrationBackup(JellyfinDb = true)] +public class RefreshCleanNames : IAsyncMigrationRoutine +{ + private readonly IStartupLogger _logger; + private readonly IDbContextFactory _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// Instance of the interface. + public RefreshCleanNames( + IStartupLogger logger, + IDbContextFactory dbProvider) + { + _logger = logger; + _dbProvider = dbProvider; + } + + /// + 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(); + appHost.Setup(x => x.ExpandVirtualPath(It.IsAny())) + .Returns((string x) => x); + appHost.Setup(x => x.ReverseVirtualPath(It.IsAny())) + .Returns((string x) => x); + + var configSection = new Mock(); + configSection + .SetupGet(x => x[It.Is(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)]) + .Returns("0"); + var config = new Mock(); + config + .Setup(x => x.GetSection(It.Is(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(); + } + + [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 Date: Tue, 9 Dec 2025 05:01:12 +0100 Subject: [PATCH 137/300] 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 @@ - + 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 @@ - + From 0b3d6676d1dc78f38cd17c04ecafe2196a291199 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 8 Dec 2025 21:01:32 -0700 Subject: [PATCH 138/300] 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 /// /// Gets activity log entries. /// - /// Optional. The record index to start at. All items with a lower index will be dropped from the results. - /// Optional. The maximum number of records to return. - /// Optional. The minimum date. Format = ISO. - /// Optional. Filter log entries if it has user id, or not. + /// The record index to start at. All items with a lower index will be dropped from the results. + /// The maximum number of records to return. + /// The minimum date. + /// Filter log entries if it has user id, or not. + /// Filter by name. + /// Filter by overview. + /// Filter by short overview. + /// Filter by type. + /// Filter by item id. + /// Filter by username. + /// Filter by log severity. + /// Specify one or more sort orders. Format: SortBy=Name,Type. + /// Sort Order.. /// Activity log returned. /// A containing the log entries. [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 sortBy, + IReadOnlyList 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; + +/// +/// Activity log sorting options. +/// +public enum ActivityLogSortBy +{ + /// + /// Sort by name. + /// + Name = 0, + + /// + /// Sort by overview. + /// + Overiew = 1, + + /// + /// Sort by short overview. + /// + ShortOverview = 2, + + /// + /// Sort by type. + /// + Type = 3, + + /* + /// + /// Sort by item name. + /// + Item = 4, + */ + + /// + /// Sort by date. + /// + DateCreated = 5, + + /// + /// Sort by username. + /// + Username = 6, + + /// + /// Sort by severity. + /// + 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; + +/// +/// A class representing a query to the activity logs. +/// +public class ActivityLogQuery : PaginatedQuery { /// - /// A class representing a query to the activity logs. + /// Gets or sets a value indicating whether to take entries with a user id. /// - public class ActivityLogQuery : PaginatedQuery - { - /// - /// Gets or sets a value indicating whether to take entries with a user id. - /// - public bool? HasUserId { get; set; } + public bool? HasUserId { get; set; } - /// - /// Gets or sets the minimum date to query for. - /// - public DateTime? MinDate { get; set; } - } + /// + /// Gets or sets the minimum date to query for. + /// + public DateTime? MinDate { get; set; } + + /// + /// Gets or sets the name filter. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the overview filter. + /// + public string? Overview { get; set; } + + /// + /// Gets or sets the short overview filter. + /// + public string? ShortOverview { get; set; } + + /// + /// Gets or sets the type filter. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the item filter. + /// + public Guid? ItemId { get; set; } + + /// + /// Gets or sets the username filter. + /// + public string? Username { get; set; } + + /// + /// Gets or sets the log level filter. + /// + public LogLevel? Severity { get; set; } + + /// + /// Gets or sets the result ordering. + /// + 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; + +/// +/// Manages the storage and retrieval of instances. +/// +public class ActivityManager : IActivityManager { + private readonly IDbContextFactory _provider; + /// - /// Manages the storage and retrieval of instances. + /// Initializes a new instance of the class. /// - public class ActivityManager : IActivityManager + /// The Jellyfin database provider. + public ActivityManager(IDbContextFactory provider) { - private readonly IDbContextFactory _provider; + _provider = provider; + } - /// - /// Initializes a new instance of the class. - /// - /// The Jellyfin database provider. - public ActivityManager(IDbContextFactory provider) + /// + public event EventHandler>? EntryCreated; + + /// + 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); } - /// - public event EventHandler>? EntryCreated; + EntryCreated?.Invoke(this, new GenericEventArgs(ConvertToOldModel(entry))); + } - /// - public async Task CreateAsync(ActivityLog entry) + /// + public async Task> 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(ConvertToOldModel(entry))); - } - - /// - public async Task> 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( - 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); } - } - /// - 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( + 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)); } } + + /// + 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 ApplyOrdering(IQueryable query, IReadOnlyCollection<(ActivityLogSortBy, SortOrder)>? sorting) + { + if (sorting is null || sorting.Count == 0) + { + return query.OrderByDescending(e => e.ActivityLog.DateCreated); + } + + IOrderedQueryable 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> 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; + +/// +/// Interface for the activity manager. +/// +public interface IActivityManager { - public interface IActivityManager - { - event EventHandler> EntryCreated; + /// + /// The event that is triggered when an entity is created. + /// + event EventHandler> EntryCreated; - Task CreateAsync(ActivityLog entry); + /// + /// Create a new activity log entry. + /// + /// The entry to create. + /// A representing the asynchronous operation. + Task CreateAsync(ActivityLog entry); - Task> GetPagedResultAsync(ActivityLogQuery query); + /// + /// Get a paged list of activity log entries. + /// + /// The activity log query. + /// The page of entries. + Task> GetPagedResultAsync(ActivityLogQuery query); - /// - /// Remove all activity logs before the specified date. - /// - /// Activity log start date. - /// A representing the asynchronous operation. - Task CleanAsync(DateTime startDate); - } + /// + /// Remove all activity logs before the specified date. + /// + /// Activity log start date. + /// A representing the asynchronous operation. + Task CleanAsync(DateTime startDate); } From f24e80701cca2398b9efea8605bd406ff36f3746 Mon Sep 17 00:00:00 2001 From: xin Date: Tue, 9 Dec 2025 06:06:17 +0200 Subject: [PATCH 139/300] 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 140/300] avoid Take(0) when limit == 0 (#14608) Co-authored-by: Evan --- .../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 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( @@ -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 ApplyQueryPaging(IQueryable 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? 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 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 Date: Tue, 9 Dec 2025 05:16:14 +0100 Subject: [PATCH 141/300] 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 @@ - - - + + + @@ -96,4 +96,4 @@ - \ No newline at end of file + 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 Date: Tue, 9 Dec 2025 13:18:07 -0500 Subject: [PATCH 142/300] 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 143/300] 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 Date: Tue, 9 Dec 2025 19:29:33 -0500 Subject: [PATCH 144/300] 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 Date: Tue, 9 Dec 2025 19:43:50 -0500 Subject: [PATCH 145/300] 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 Date: Wed, 10 Dec 2025 12:23:05 -0800 Subject: [PATCH 146/300] 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 Date: Thu, 11 Dec 2025 22:15:19 -0500 Subject: [PATCH 147/300] 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 /// The type of item. /// The IEnumerable{Enum}. public static IEnumerable GetUniqueFlags(this T flags) - where T : Enum + where T : struct, Enum { - foreach (Enum value in Enum.GetValues(flags.GetType())) + foreach (T value in Enum.GetValues()) { 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 148/300] 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 Date: Sat, 13 Dec 2025 16:26:22 +0100 Subject: [PATCH 149/300] 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; - -/// -/// Removes /emby and /mediabrowser from requested route. -/// -public class LegacyEmbyRouteRewriteMiddleware -{ - private const string EmbyPath = "/emby"; - private const string MediabrowserPath = "/mediabrowser"; - - private readonly RequestDelegate _next; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The next delegate in the pipeline. - /// The logger. - public LegacyEmbyRouteRewriteMiddleware( - RequestDelegate next, - ILogger logger) - { - _next = next; - _logger = logger; - } - - /// - /// Executes the middleware action. - /// - /// The current HTTP context. - /// The async task. - 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(); } - - /// - /// Adds /emby and /mediabrowser route trimming to the application pipeline. - /// - /// - /// This must be injected before any path related middleware. - /// - /// The application builder. - /// The updated application builder. - public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder) - { - return appBuilder.UseMiddleware(); - } } } 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 Date: Sun, 14 Dec 2025 00:29:28 +0900 Subject: [PATCH 150/300] 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 +{ + /// + /// Helper class to retrieve basic metadata from a book filename. + /// + 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(@"^(?.+?)((\s\((?[0-9]{4})\))?)\s#(?[0-9]+)((\s\(of\s(?[0-9]+)\))?)((\s\((?[0-9]{4})\))?)$"), + new Regex(@"^(?.+?)\s\((?.+?),\s#(?[0-9]+)\)((\s\((?[0-9]{4})\))?)$"), + new Regex(@"^(?[0-9]+)\s\-\s(?.+?)((\s\((?[0-9]{4})\))?)$"), + new Regex(@"(?.*)\((?[0-9]{4})\)"), + // last resort matches the whole string as the name + new Regex(@"(?.*)") + ]; + + /// + /// Parse a filename name to retrieve the book name, series name, index, and year. + /// + /// Book filename to parse for information. + /// Returns object. + 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 +{ + /// + /// Data object used to pass metadata parsed from a book filename. + /// + public class BookFileNameParserResult + { + /// + /// Initializes a new instance of the class. + /// + public BookFileNameParserResult() + { + Name = null; + Index = null; + Year = null; + SeriesName = null; + } + + /// + /// Gets or sets the name of the book. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the book index. + /// + public int? Index { get; set; } + + /// + /// Gets or sets the publication year. + /// + public int? Year { get; set; } + + /// + /// Gets or sets the series name. + /// + 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 151/300] 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 Date: Sat, 13 Dec 2025 08:43:49 -0700 Subject: [PATCH 152/300] 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 } /// - 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); + } } /// 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 /// The cancellation token. void SaveItems(IReadOnlyList items, CancellationToken cancellationToken); - void SaveImages(BaseItem item); + Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); /// /// Retrieves the item. From c4e8180b3c591b09bd76e7f49c620fc7cde7442b Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 15 Dec 2025 09:20:41 -0500 Subject: [PATCH 153/300] 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 Date: Mon, 15 Dec 2025 13:47:14 +0100 Subject: [PATCH 154/300] 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?= Date: Tue, 16 Dec 2025 10:23:15 -0500 Subject: [PATCH 155/300] 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 156/300] 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 Date: Wed, 17 Dec 2025 17:11:14 -0500 Subject: [PATCH 157/300] 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 Date: Thu, 18 Dec 2025 22:56:33 +0100 Subject: [PATCH 158/300] 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 Date: Thu, 18 Dec 2025 23:07:35 -0500 Subject: [PATCH 159/300] 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 Date: Thu, 18 Dec 2025 01:43:23 -0500 Subject: [PATCH 160/300] 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?= Date: Sat, 20 Dec 2025 04:36:38 +0100 Subject: [PATCH 161/300] 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 /// The record index to start at. All items with a lower index will be dropped from the results. /// The maximum number of records to return. /// The minimum date. + /// The maximum date. /// Filter log entries if it has user id, or not. /// Filter by name. /// Filter by overview. @@ -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 /// public DateTime? MinDate { get; set; } + /// + /// Gets or sets the maximum date to query for. + /// + public DateTime? MaxDate { get; set; } + /// /// Gets or sets the name filter. /// 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 fa99b1d81c5e8802e0aaecf4718c6c1413f2a1ac Mon Sep 17 00:00:00 2001 From: lostb1t Date: Sun, 5 Oct 2025 12:53:17 +0200 Subject: [PATCH 162/300] 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 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 GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private async Task 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 Date: Sun, 5 Oct 2025 16:06:59 +0200 Subject: [PATCH 163/300] 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 Date: Sun, 5 Oct 2025 18:01:00 +0200 Subject: [PATCH 164/300] 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 Date: Sat, 29 Nov 2025 10:11:18 +0100 Subject: [PATCH 165/300] 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 Date: Sat, 29 Nov 2025 10:20:18 +0100 Subject: [PATCH 166/300] 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 Date: Mon, 1 Dec 2025 10:24:09 +0100 Subject: [PATCH 167/300] 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 Date: Mon, 1 Dec 2025 12:00:08 +0100 Subject: [PATCH 168/300] 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 0e73a56a457cd5b91673da2e42304066aadffbdb Mon Sep 17 00:00:00 2001 From: Translation expert Date: Thu, 25 Dec 2025 12:20:18 -0500 Subject: [PATCH 169/300] 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 170/300] 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 @@ - + From c30654c33c0561ee7f3b7d096038f9604a111003 Mon Sep 17 00:00:00 2001 From: MrPlow Date: Sat, 27 Dec 2025 02:16:02 -0500 Subject: [PATCH 171/300] 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 Date: Sun, 28 Dec 2025 07:22:11 -0500 Subject: [PATCH 172/300] 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 Backported-by: Bond_009 --- 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 Date: Sun, 28 Dec 2025 07:22:12 -0500 Subject: [PATCH 173/300] Backport pull request #15662 from jellyfin/release-10.11.z Fix blocking in async context in LimitedConcurrencyLibraryScheduler Original-merge: d91adb5d54ed706198cd3066608107bbfeedebc1 Merged-by: Bond-009 Backported-by: Bond_009 --- 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 /// private readonly Lock _taskLock = new(); - private readonly BlockingCollection _tasks = new(); + private readonly Channel _tasks = Channel.CreateUnbounded(); 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 Date: Sun, 28 Dec 2025 07:22:13 -0500 Subject: [PATCH 174/300] Backport pull request #15666 from jellyfin/release-10.11.z Fix unnecessary database JOINs in ApplyNavigations Original-merge: 4cdd8c8233cc8e2b4ced9be5b7ddbd48f190a3b9 Merged-by: crobibero Backported-by: Bond_009 --- .../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 ApplyNavigations(IQueryable 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 175/300] Backport pull request #15680 from jellyfin/release-10.11.z Fix ItemAdded event triggering when updating metadata Original-merge: fb65f8f8532fbad22089a3a3cfb4d9237c71c567 Merged-by: crobibero Backported-by: Bond_009 --- 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); } /// From 5d50ff5f81872cc9c2b1611c878162a39714a1a8 Mon Sep 17 00:00:00 2001 From: ivanjx Date: Sun, 28 Dec 2025 07:22:15 -0500 Subject: [PATCH 176/300] Backport pull request #15681 from jellyfin/release-10.11.z Fix symlinked file size Original-merge: d32f487e8e4762bba740b586285b663712eda69a Merged-by: crobibero Backported-by: Bond_009 --- 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); } /// From 55570043759bcfa3c76df7e94d3303257171c970 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 28 Dec 2025 07:22:17 -0500 Subject: [PATCH 177/300] Backport pull request #15689 from jellyfin/release-10.11.z Use original name for MusicAritist matching Original-merge: 4c5a3fbff34a603ff0344e0b42d07bc17f31f92c Merged-by: crobibero Backported-by: Bond_009 --- 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() .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 Date: Sun, 28 Dec 2025 07:22:18 -0500 Subject: [PATCH 178/300] 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 Backported-by: Bond_009 --- 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 Date: Sun, 28 Dec 2025 07:22:19 -0500 Subject: [PATCH 179/300] Backport pull request #15698 from jellyfin/release-10.11.z Fix stack overflow during scan (#15000) Original-merge: dde70fd8a2007f52f87546eb3c3acf8963333c4c Merged-by: crobibero Backported-by: Bond_009 --- .../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(); } /// From b9cf26db2f3d219340a17951c289a230d0ccf31a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 28 Dec 2025 07:22:20 -0500 Subject: [PATCH 180/300] Backport pull request #15746 from jellyfin/release-10.11.z Skip invalid ignore rules Original-merge: 6e60634c9f078cc01e343b07a0a6b2a5c230478c Merged-by: crobibero Backported-by: Bond_009 --- .../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); + } + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory) + => CheckIgnoreRules(path, rules, isDirectory, IsWindows); + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// Whether to normalize backslashes to forward slashes (for Windows paths). + /// True if the path should be ignored. + 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 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 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 181/300] Backport pull request #15752 from jellyfin/release-10.11.z Fix case sensitivity edge case Original-merge: b50ce1ad6b3239245897a648dd24cf407138abfc Merged-by: Bond-009 Backported-by: Bond_009 --- 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 182/300] 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 Backported-by: Bond_009 --- 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(itemId, User.GetUserId()); + var item = _libraryManager.GetItemById(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 183/300] 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 Backported-by: Bond_009 --- .../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 184/300] Backport pull request #15767 from jellyfin/release-10.11.z Fix collections display order Original-merge: 22da5187c88a60118cac03bc77427efa72b97888 Merged-by: crobibero Backported-by: Bond_009 --- 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 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 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 185/300] Backport pull request #15768 from jellyfin/release-10.11.z Fix NullReferenceException in ApplyOrder method Original-merge: b617c62f8ef53848d136155a02e9d3fbffc7b365 Merged-by: crobibero Backported-by: Bond_009 --- 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 ApplyOrder(IQueryable 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 Date: Sun, 28 Dec 2025 07:22:27 -0500 Subject: [PATCH 186/300] Backport pull request #15776 from jellyfin/release-10.11.z Fix AV1 decoding hang regression on RK3588 Original-merge: 035b5895b051edf3f8bb653e52555fb3d63f3544 Merged-by: crobibero Backported-by: Bond_009 --- 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 187/300] Backport pull request #15786 from jellyfin/release-10.11.z Fix parental rating filtering with sub-scores Original-merge: 5804d6840c0276d3aef81bfec6af82e496672f01 Merged-by: crobibero Backported-by: Bond_009 --- 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 Date: Sun, 28 Dec 2025 07:22:30 -0500 Subject: [PATCH 188/300] Backport pull request #15793 from jellyfin/release-10.11.z Prefer US rating on fallback Original-merge: 156761405e7fd5308474a7e6301839ae7c694dfa Merged-by: crobibero Backported-by: Bond_009 --- .../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 Date: Sun, 28 Dec 2025 07:22:31 -0500 Subject: [PATCH 189/300] Backport pull request #15807 from jellyfin/release-10.11.z Enforce more strict webm check Original-merge: 8379b4634aeaf9827d07a41cf9ba8fd80c8c323e Merged-by: crobibero Backported-by: Bond_009 --- .../Probing/ProbeResultNormalizer.cs | 9 +- .../Probing/ProbeResultNormalizerTests.cs | 12 ++ .../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(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 Date: Sun, 28 Dec 2025 07:22:32 -0500 Subject: [PATCH 190/300] 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 Backported-by: Bond_009 --- .../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 Date: Sun, 28 Dec 2025 07:22:33 -0500 Subject: [PATCH 191/300] Backport pull request #15826 from jellyfin/release-10.11.z add CultureDto cache Original-merge: 1805f2259f44aba0ca97ff0de2ad0b0a3614fa03 Merged-by: crobibero Backported-by: Bond_009 --- .../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 _cultureCache = new(StringComparer.OrdinalIgnoreCase); private List _cultures = []; private FrozenDictionary _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 /// 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); } /// From 7f0e71578dc278da36c5efeae2fe2674e4677230 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 28 Dec 2025 07:22:34 -0500 Subject: [PATCH 192/300] 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 Backported-by: Bond_009 --- 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 Date: Sun, 28 Dec 2025 07:22:36 -0500 Subject: [PATCH 193/300] Backport pull request #15834 from jellyfin/release-10.11.z Fix video lacking SAR and DAR are marked as anamorphic Original-merge: 9470439cfa1eaf7cb9717f16031b020cedab516a Merged-by: crobibero Backported-by: Bond_009 --- .../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 Date: Sun, 28 Dec 2025 07:22:37 -0500 Subject: [PATCH 194/300] Backport pull request #15835 from jellyfin/release-10.11.z Use hvc1 codectag for Dolby Vision 8.4 Original-merge: 18096e48e0c72b08598a06e5512e6eb81d91fb51 Merged-by: crobibero Backported-by: Bond_009 --- 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 195/300] Backport pull request #15858 from jellyfin/release-10.11.z Fix playlist item de-duplication Original-merge: 78e3702cb064fc664ed1a658ad534cf66f5373d3 Merged-by: crobibero Backported-by: Bond_009 --- 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 } 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 196/300] 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 @@ - + From 82b2e7773f2caa29e6f3c41e3067cd7e32dee5f6 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 14 Oct 2025 20:56:40 +0200 Subject: [PATCH 197/300] 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 GetReleaseGroupResult(IEnumerable? releaseSearchResults) + private IEnumerable GetReleaseGroupResult(IEnumerable? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (releaseSearchResults is null) { @@ -138,7 +141,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider Date: Tue, 14 Oct 2025 20:50:26 +0200 Subject: [PATCH 198/300] 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 GetReleaseGroupResult(IEnumerable? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default) + private async IAsyncEnumerable GetReleaseGroupResultAsync(IEnumerable? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (releaseSearchResults is null) { @@ -141,7 +141,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider Date: Mon, 29 Dec 2025 05:23:28 -0500 Subject: [PATCH 199/300] 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 Date: Mon, 29 Dec 2025 10:10:56 -0500 Subject: [PATCH 200/300] 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 Date: Mon, 29 Dec 2025 08:44:22 -0500 Subject: [PATCH 201/300] 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 e5fb07170888f5c77df9a686b1a73bafae26c784 Mon Sep 17 00:00:00 2001 From: stevenaw Date: Wed, 31 Dec 2025 09:26:31 -0500 Subject: [PATCH 202/300] 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 { /// @@ -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 Date: Wed, 31 Dec 2025 14:20:41 +0000 Subject: [PATCH 203/300] 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 Date: Wed, 31 Dec 2025 14:30:25 +0000 Subject: [PATCH 204/300] 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 Date: Wed, 31 Dec 2025 20:51:37 +0000 Subject: [PATCH 205/300] 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 Date: Fri, 2 Jan 2026 01:46:17 +0000 Subject: [PATCH 206/300] 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< Date: Fri, 2 Jan 2026 02:46:51 +0100 Subject: [PATCH 207/300] 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 @@ - - + + 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(); + c.SchemaFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); @@ -342,25 +343,6 @@ namespace Jellyfin.Server.Extensions } }); - /* - * Support BlurHash dictionary - */ - options.MapType>>(() => - 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>(() => new OpenApiSchema @@ -373,21 +355,6 @@ namespace Jellyfin.Server.Extensions } }); - // Manually describe Flags enum. - options.MapType(() => - 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(() => 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() - .Select(e => new OpenApiString(e)) - .Cast() - .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 _logger; /// /// Initializes a new instance of the class. @@ -31,15 +33,18 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider /// The api descriptions provider. /// The schema generator. /// The memory cache. + /// The logger. public CachingOpenApiProvider( IOptions optionsAccessor, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, ISchemaGenerator schemaGenerator, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + ILogger logger) { _swaggerGeneratorOptions = optionsAccessor.Value; _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); _memoryCache = memoryCache; + _logger = logger; } /// @@ -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; + +/// +/// Schema filter to ensure flags enums are represented correctly in OpenAPI. +/// +/// +/// 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. +/// +public class FlagsEnumSchemaFilter : ISchemaFilter +{ + /// + 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 Date: Mon, 29 Dec 2025 17:51:50 +0100 Subject: [PATCH 208/300] 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 Date: Thu, 1 Jan 2026 22:27:50 +0100 Subject: [PATCH 209/300] 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-changes-reply.md - echo "
" >> openapi-changes-reply.md - echo "Changes in OpenAPI specification found. Expand to see details." >> openapi-changes-reply.md - echo "" >> openapi-changes-reply.md - echo "$body" >> openapi-changes-reply.md - echo "" >> openapi-changes-reply.md - echo "
" >> 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: | - - - 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 210/300] 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 @@ - + From bfae788a44d7c5640bd23c6f433533ef16f30107 Mon Sep 17 00:00:00 2001 From: Dzmitry Zubialevich Date: Fri, 2 Jan 2026 05:40:57 -0500 Subject: [PATCH 211/300] 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 Date: Mon, 5 Jan 2026 12:19:26 +0100 Subject: [PATCH 212/300] 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 Date: Mon, 5 Jan 2026 06:22:44 -0500 Subject: [PATCH 213/300] 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 Date: Mon, 5 Jan 2026 04:34:05 -0500 Subject: [PATCH 214/300] 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 Date: Mon, 5 Jan 2026 21:08:26 -0500 Subject: [PATCH 215/300] 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()!; } /// @@ -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()!; } /// @@ -1159,7 +1159,7 @@ public sealed class BaseItemRepository return type.GetCustomAttribute() == 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 /// Logger. /// The application server Host. /// If only mapping should be processed. - /// A mapped BaseItem. - /// Will be thrown if an invalid serialisation is requested. - public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false) + /// A mapped BaseItem, or null if the item type is unknown. + 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(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().ToArray()); + return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast().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(); + + // Act + BaseItemRepository.DeserializeBaseItem(entity, loggerMock.Object, null, false); + + // Assert + loggerMock.Verify( + x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("unknown type", StringComparison.OrdinalIgnoreCase)), + It.IsAny(), + It.IsAny>()), + 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 Date: Mon, 5 Jan 2026 23:03:22 -0500 Subject: [PATCH 216/300] 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}"); } + /// + /// Extracts and validates the iterations parameter from a password hash. + /// + /// The password hash containing parameters. + /// The number of iterations. + /// Thrown when iterations parameter is missing or invalid. + 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; + } + /// 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(() => _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(() => _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(() => _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(() => _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(() => _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 Date: Tue, 6 Jan 2026 23:15:06 +0800 Subject: [PATCH 217/300] Add TrueHD and DTS codes string for HLS Signed-off-by: nyanmisaka --- 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 ///
public const string OPUS = "Opus"; + /// + /// Codec name for TRUEHD. + /// + public const string TRUEHD = "mlpa"; + /// /// Gets a MP3 codec string. /// @@ -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; } + /// + /// Gets an TRUEHD codec string. + /// + /// TRUEHD codec string. + public static string GetTRUEHDString() + { + return TRUEHD; + } + + /// + /// Gets an DTS codec string. + /// + /// DTS profile. + /// DTS codec string. + 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"; + } + /// /// Gets a H.264 codec string. /// 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 218/300] 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 @@ - + From e233eee07b318ee59d528e715ad0ba490013b1db Mon Sep 17 00:00:00 2001 From: SilentSkies Date: Fri, 9 Jan 2026 17:14:09 -0500 Subject: [PATCH 219/300] 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 Date: Fri, 9 Jan 2026 17:22:26 -0500 Subject: [PATCH 220/300] 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 Date: Fri, 9 Jan 2026 17:33:27 -0500 Subject: [PATCH 221/300] 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 222/300] Backport pull request #15931 from jellyfin/release-10.11.z Fix tag inheritance for Continue Watching queries Original-merge: 559e0088e5316a857f764a848e76e4fbd62fa834 Merged-by: crobibero Backported-by: Bond_009 --- .../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 223/300] Backport pull request #15950 from jellyfin/release-10.11.z Revert "always sort season by index number" Original-merge: 32d2414de0b3d119929c063714b6e4f0023893c7 Merged-by: Bond-009 Backported-by: Bond_009 --- 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 224/300] 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 Backported-by: Bond_009 --- 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(); } + catch (NotSupportedException ex) + { + _logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest); + return Array.Empty(); + } 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 Date: Sat, 10 Jan 2026 06:39:07 -0500 Subject: [PATCH 225/300] 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 226/300] 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 227/300] 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 Date: Tue, 11 Nov 2025 17:41:46 +0100 Subject: [PATCH 228/300] 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 @@ - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -81,12 +76,8 @@ - - - - - - + + 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 @@ - net9.0 + net10.0 false true true 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 @@ - net9.0 + net10.0 false true 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 @@ - @@ -39,7 +38,7 @@ - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 true 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 @@ - net9.0 + net10.0 false true true 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(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(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(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 @@ - net9.0 + net10.0 false true @@ -27,7 +27,6 @@ - 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 @@ jellyfin Exe - net9.0 + net10.0 false false true @@ -44,9 +44,6 @@ - - - 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 @@ - - - - - - net9.0 + net10.0 false true true 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 /// An . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. - public static bool TryParseToSubnet(ReadOnlySpan value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) + public static bool TryParseToSubnet(ReadOnlySpan 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 /// The broadcast address. 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 @@ - - @@ -36,7 +34,7 @@ - net9.0 + net10.0 false true true 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 @@ - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 false true @@ -26,7 +26,6 @@ - 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 @@ - net9.0 + net10.0 false true true @@ -37,13 +37,10 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive - - 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 @@ - @@ -28,7 +27,7 @@ - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 false true 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 @@ Exe - net9.0 + net10.0 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 @@ Exe - net9.0 + net10.0 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 @@  - net9.0 + net10.0 false true 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 @@  - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 false true true 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 @@ - net9.0 + net10.0 true @@ -13,7 +13,6 @@ - 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 @@ - net9.0 + net10.0 true @@ -12,9 +12,6 @@ - - - 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 @@ - net9.0 + net10.0 true @@ -22,10 +22,6 @@ - - - - <_Parameter1>Jellyfin.MediaEncoding.Keyframes.Tests 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 @@ - net9.0 + net10.0 false true 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 @@ - net9.0 + net10.0 false 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 @@ - 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 @@ - net9.0 + net10.0 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 @@ - 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 @@ - 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 Date: Sun, 16 Nov 2025 18:59:50 +0100 Subject: [PATCH 229/300] 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 /// Collection of . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. - public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList? result, bool negated = false) + public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList? 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(); + List? 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; } /// - /// Try parsing a string into an , respecting exclusions. - /// Inputs without a subnet mask will be represented as with a single IP. + /// Try parsing a string into an , respecting exclusions. + /// Inputs without a subnet mask will be represented as with a single IP. /// /// Input string to be parsed. - /// An . + /// An . /// Boolean signaling if negated or not negated values should be parsed. /// True if parsing was successful. - public static bool TryParseToSubnet(ReadOnlySpan value, out IPNetwork result, bool negated = false) + public static bool TryParseToSubnet(ReadOnlySpan 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(); + ? excludedSubnets.Select(x => x.Subnet).ToArray() + : Array.Empty(); } } @@ -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))); } /// @@ -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))); } /// @@ -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 Date: Sun, 16 Nov 2025 19:31:45 +0100 Subject: [PATCH 230/300] 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 OrderByString(this IEnumerable list, Func 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 -{ - /// - /// Alphanumeric . - /// - public class AlphanumericComparator : IComparer - { - /// - /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other. - /// - /// The first object to compare. - /// The second object to compare. - /// A signed integer that indicates the relative values of x and y. - 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; - } - - /// - 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 Date: Thu, 27 Nov 2025 18:00:27 +0100 Subject: [PATCH 231/300] 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 Date: Thu, 27 Nov 2025 18:03:10 +0100 Subject: [PATCH 232/300] 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 Date: Thu, 27 Nov 2025 18:07:07 +0100 Subject: [PATCH 233/300] 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 Date: Thu, 27 Nov 2025 18:11:05 +0100 Subject: [PATCH 234/300] 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 Date: Thu, 27 Nov 2025 18:16:47 +0100 Subject: [PATCH 235/300] 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 Date: Wed, 14 Jan 2026 18:59:02 +0100 Subject: [PATCH 236/300] 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 @@ - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -58,10 +58,10 @@ - + - + @@ -77,7 +77,7 @@ - + From 6a9bb060ebb7a5e9d4a9a016403f0fad0ad63218 Mon Sep 17 00:00:00 2001 From: rimasx Date: Thu, 15 Jan 2026 09:18:56 -0500 Subject: [PATCH 237/300] 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 238/300] 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 @@ - + 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 239/300] 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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. The maximum streaming bitrate. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. The maximum streaming bitrate. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. The maximum streaming bitrate. @@ -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 /// Optional. The maximum audio bit depth. /// Optional. Whether to enable remote media. /// Optional. Whether to enable Audio Encoding. - /// Optional. Whether to break on non key frames. /// Whether to enable redirection. Defaults to true. /// Audio stream returned. /// Redirected to remote audio stream. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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 /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. /// Whether or not to allow copying of the video stream url. /// Whether or not to allow copying of the audio stream url. - /// Optional. Whether to break on non key frames. /// Optional. Specify a specific audio sample rate, e.g. 44100. /// Optional. The maximum audio bit depth. /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. @@ -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; } - /// /// Gets or sets the audio sample rate. /// 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; } /// @@ -301,4 +303,10 @@ public class EncodingOptions /// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for. /// public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; } + + /// + /// Gets or sets the method used for audio seeking in HLS. + /// + [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 +{ + /// + /// An enum representing the options to seek the input audio stream when + /// transcoding HLS segments. + /// + public enum HlsAudioSeekStrategy + { + /// + /// 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. + /// + DisableAccurateSeek = 0, + + /// + /// 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. + /// + 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 /// The minimum segments count. public int? MinSegments { get; set; } - /// - /// Gets or sets a value indicating whether the stream can be broken on non-keyframes. - /// - public bool BreakOnNonKeyFrames { get; set; } - /// /// Gets or sets a value indicating whether the stream requires AVC. /// @@ -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 /// [DefaultValue(false)] [XmlAttribute("breakOnNonKeyFrames")] - public bool BreakOnNonKeyFrames { get; set; } + [Obsolete("This is always false")] + public bool? BreakOnNonKeyFrames { get; set; } /// /// 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 240/300] 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 @@ - - + + 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 241/300] Backport pull request #15808 from jellyfin/release-10.11.z Trim music artist names Original-merge: 093cfc3f3b72a6bea71cb96ced180a9ac257d537 Merged-by: crobibero Backported-by: Bond_009 --- 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 Date: Sun, 18 Jan 2026 11:30:37 -0500 Subject: [PATCH 242/300] Backport pull request #15872 from jellyfin/release-10.11.z Be more strict about PersonType assignment Original-merge: b56de6493f67cd1cdc43b47745ae66908d1aef41 Merged-by: crobibero Backported-by: Bond_009 --- .../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 243/300] 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 Backported-by: Bond_009 --- 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); + /// + 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(); } + /// + 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); + } + } + /// 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); + /// /// Validates that images within the item are still on the filesystem. /// 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 a Task that can be awaited. Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// 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); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// 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 result, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult 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 244/300] Backport pull request #15965 from jellyfin/release-10.11.z Add mblink creation logic to library update endpoint. Original-merge: 22d593b8e986ecdb42fb1e618bfcf833b0a6f118 Merged-by: crobibero Backported-by: Bond_009 --- .../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. /// void QueueLibraryScan(); + + /// + /// Add mblink file for a media path. + /// + /// The path to the virtualfolder. + /// The new virtualfolder. + public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo); } } From c4ffc357a3d3658526f6fd879364145333eea6b0 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 18 Jan 2026 11:30:41 -0500 Subject: [PATCH 245/300] Backport pull request #15983 from jellyfin/release-10.11.z Prioritize better matches on search Original-merge: a518160a6ff471541b7daae6d54c8b896bb1f2e6 Merged-by: crobibero Backported-by: Bond_009 --- .../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? 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 }; } + + /// + /// 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). + /// + /// The search term to match against. + /// An expression that returns an integer representing match quality (lower is better). + public static Expression> 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 246/300] Backport pull request #16020 from jellyfin/release-10.11.z Fix birthplace not saving correctly Original-merge: 49775b1f6aaa958f19a0ee4ea05bb9aab78c6b5b Merged-by: crobibero Backported-by: Bond_009 --- 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 247/300] 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 Backported-by: Bond_009 --- 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 248/300] Backport pull request #16046 from jellyfin/release-10.11.z Restore weekly refresh for library folder images Original-merge: 338b480217499bc37ce4bbe214aea0f1f7d9eb6a Merged-by: cvium Backported-by: Bond_009 --- .../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 Date: Sun, 18 Jan 2026 22:52:51 +0530 Subject: [PATCH 249/300] 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 --- 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; /// - /// Gets a value indicating whether IP4 is enabled. + /// Gets a value indicating whether IPv4 is enabled. /// public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4; @@ -361,7 +361,7 @@ public class NetworkManager : INetworkManager, IDisposable } /// - /// Filteres a list of bind addresses and exclusions on available interfaces. + /// Filters a list of bind addresses and exclusions on available interfaces. /// /// The network config to be filtered by. /// A list of possible interfaces to be filtered. From 977d1c38b23347e79e09328e88c081246942f97f Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 18 Jan 2026 20:04:52 -0500 Subject: [PATCH 250/300] 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 Date: Sun, 18 Jan 2026 22:16:33 -0500 Subject: [PATCH 251/300] 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 Date: Mon, 19 Jan 2026 01:43:13 -0500 Subject: [PATCH 252/300] 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 253/300] 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 Date: Sat, 24 Jan 2026 18:09:20 -0500 Subject: [PATCH 254/300] 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 Date: Sat, 24 Jan 2026 20:27:03 -0500 Subject: [PATCH 255/300] 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 Date: Sat, 24 Jan 2026 20:35:10 -0500 Subject: [PATCH 256/300] 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 Date: Sat, 24 Jan 2026 20:54:13 -0500 Subject: [PATCH 257/300] 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 Date: Sat, 24 Jan 2026 23:20:11 -0500 Subject: [PATCH 258/300] 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 Date: Sun, 25 Jan 2026 04:50:00 -0500 Subject: [PATCH 259/300] 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 Date: Sun, 25 Jan 2026 21:39:57 +0100 Subject: [PATCH 260/300] 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(); } - - /// - /// Updates the path to the media encoder. - /// - /// Media encoder path form body. - /// Media encoder path updated. - /// Status. - [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(); } - /// - /// Gets network paths. - /// - /// Empty array returned. - /// List of entries. - [Obsolete("This endpoint is obsolete.")] - [HttpGet("NetworkShares")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNetworkShares() - { - _logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares"); - return Array.Empty(); - } - /// /// Gets available drives from the server's file system. /// 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 } } - /// - /// Old version of using a GET method. - /// Still available to avoid breaking compatibility. - /// - /// The result of . - [Obsolete("Use POST request instead")] - [HttpGet("Initiate")] - [ApiExplorerSettings(IgnoreApi = true)] - public Task> InitiateQuickConnectLegacy() => InitiateQuickConnect(); - /// /// Attempts to retrieve authentication information. /// 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 /// Optional. Include user data. /// Optional. Starting date of shows to show in Next Up section. /// Whether to enable the total records count. Defaults to true. - /// Whether to disable sending the first episode in a series as next up. /// Whether to include resumable episodes in next up results. /// Whether to include watched episodes in next up results. /// A with the next up episodes. @@ -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); - /// - /// Updates a user's easy password. - /// - /// The user id. - /// The request. - /// Password successfully reset. - /// User is not allowed to update the password. - /// User not found. - /// A indicating success or a or a on failure. - [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(); - } - /// /// Updates a user. /// 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; - -/// -/// Media Encoder Path Dto. -/// -public class MediaEncoderPathDto -{ - /// - /// Gets or sets media encoder path. - /// - public string Path { get; set; } = null!; - - /// - /// Gets or sets media encoder path type. - /// - 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 /// [Required] public bool EnableRemoteAccess { get; set; } - - /// - /// Gets or sets a value indicating whether enable automatic port mapping. - /// - [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; - -/// -/// The update user easy password request body. -/// -public class UpdateUserEasyPassword -{ - /// - /// Gets or sets the new sha1-hashed password. - /// - public string? NewPassword { get; set; } - - /// - /// Gets or sets the new password. - /// - public string? NewPw { get; set; } - - /// - /// Gets or sets a value indicating whether to reset the password. - /// - 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 Date: Tue, 27 Jan 2026 11:13:32 +0100 Subject: [PATCH 261/300] 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 Date: Tue, 27 Jan 2026 14:12:25 -0500 Subject: [PATCH 262/300] 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 263/300] 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 LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory) + private async Task LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory, CancellationToken cancellationToken) { var utilities = new OpfReader(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 GetFromZip(BaseItem item) + private async Task 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 264/300] 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 265/300] Backport pull request #15816 from jellyfin/release-10.11.z Fix artist display order Original-merge: a2b1936e73f6638bf07d5e1afd339a1e4404027a Merged-by: joshuaboniface Backported-by: Bond_009 --- 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().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().ToArray()); + + var result = new Dictionary(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 Date: Wed, 28 Jan 2026 12:11:24 -0500 Subject: [PATCH 266/300] 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 Backported-by: Bond_009 --- 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 267/300] Backport pull request #16066 from jellyfin/release-10.11.z Fix TMDB crew department mapping Original-merge: 673f617994da6ff6a45cf428a3ea47de59edc6c5 Merged-by: crobibero Backported-by: Bond_009 --- 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 /// The Jellyfin person type. 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 268/300] Backport pull request #16071 from jellyfin/release-10.11.z Rehydrate cached UserData after reattachment Original-merge: 95d08b264f68a4348d18746543882356465be3b0 Merged-by: crobibero Backported-by: Bond_009 --- .../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 269/300] Backport pull request #16077 from jellyfin/release-10.11.z Revert hidden directory ignore pattern Original-merge: 644327eb762a907328c68ab9f5d61a151cd96897 Merged-by: crobibero Backported-by: Bond_009 --- 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 270/300] Backport pull request #16098 from jellyfin/release-10.11.z Fix random sort returning duplicate items Original-merge: a37ead86df161d6a50d280dcac6f11294301c7e8 Merged-by: nielsvanvelzen Backported-by: Bond_009 --- .../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(); + } + + 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 Date: Wed, 28 Jan 2026 12:11:30 -0500 Subject: [PATCH 271/300] Backport pull request #16109 from jellyfin/release-10.11.z Fix SessionInfoWebSocketListener not using SessionInfoDto Original-merge: e65aff8bc67e3cc97d2ebe141de9ff6a8681d792 Merged-by: nielsvanvelzen Backported-by: Bond_009 --- .../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) + /// + 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; /// /// Class SessionInfoWebSocketListener. /// -public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener, WebSocketListenerState> +public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener, WebSocketListenerState> { private readonly ISessionManager _sessionManager; private bool _disposed; @@ -52,24 +53,26 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener /// Task{SystemInfo}. - protected override Task> GetDataToSend() + protected override Task> GetDataToSend() { - return Task.FromResult(_sessionManager.Sessions); + return Task.FromResult(_sessionManager.Sessions.Select(_sessionManager.ToSessionInfoDto)); } /// - protected override Task> GetDataToSendForConnection(IWebSocketConnection connection) + protected override Task> 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)); } /// 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 /// The session id or playsession id. /// Task. Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId); + + /// + /// Gets the dto for session info. + /// + /// The session info. + /// of the session. + SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo); } } 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 272/300] 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 @@ - + From 7b10888c95c814af342926269403b93677760042 Mon Sep 17 00:00:00 2001 From: Riri Date: Sun, 1 Feb 2026 15:33:07 +0800 Subject: [PATCH 273/300] 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 Date: Sun, 1 Feb 2026 09:23:54 -0500 Subject: [PATCH 274/300] 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 Date: Sun, 1 Feb 2026 06:26:49 -0500 Subject: [PATCH 275/300] 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 8083ab78b558e4081caf4fd321e5ad8f74844f3e Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Mon, 2 Feb 2026 20:46:28 +0100 Subject: [PATCH 276/300] 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 Date: Tue, 3 Feb 2026 01:16:25 -0500 Subject: [PATCH 277/300] 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(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, true, out _)) + { + _logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType); displayPreferences.CustomPrefs.Remove(key); } } From 7c200899d71cc255feb58a110f0fce244bffa0d8 Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Mon, 2 Feb 2026 05:23:44 -0500 Subject: [PATCH 278/300] 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 Date: Mon, 2 Feb 2026 05:23:53 -0500 Subject: [PATCH 279/300] 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 Date: Tue, 3 Feb 2026 17:41:01 -0500 Subject: [PATCH 280/300] 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 dbProvider, I /// public void UpdatePeople(Guid itemId, IReadOnlyList 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 Date: Thu, 5 Feb 2026 16:39:52 -0500 Subject: [PATCH 281/300] 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 Date: Fri, 6 Feb 2026 17:04:38 -0500 Subject: [PATCH 282/300] 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 283/300] 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 @@ - + 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 284/300] 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 @@ - + 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 285/300] 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 Date: Sat, 7 Feb 2026 18:50:08 +0000 Subject: [PATCH 286/300] 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 287/300] 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 @@ - + From 558b31f3861ce6de3bbf7991fd125dd43c26bc69 Mon Sep 17 00:00:00 2001 From: Vincenzo Reale Date: Mon, 9 Feb 2026 05:07:59 -0500 Subject: [PATCH 288/300] 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 Date: Mon, 9 Feb 2026 05:11:00 -0500 Subject: [PATCH 289/300] 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 Date: Mon, 9 Feb 2026 05:42:16 -0500 Subject: [PATCH 290/300] 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 Date: Mon, 9 Feb 2026 06:31:26 -0500 Subject: [PATCH 291/300] 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 Date: Mon, 9 Feb 2026 16:28:49 -0500 Subject: [PATCH 292/300] 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 Date: Tue, 10 Feb 2026 04:34:35 -0500 Subject: [PATCH 293/300] 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 294/300] 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 295/300] 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 @@ - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -77,7 +77,7 @@ - + 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 296/300] 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 @@ - + From dce91cf8c8d01135c10058614551f2f7f70a633c Mon Sep 17 00:00:00 2001 From: saltpi Date: Sat, 14 Feb 2026 05:57:23 -0500 Subject: [PATCH 297/300] Backport pull request #16116 from jellyfin/release-10.11.z Fix TMDB image URLs missing size parameter Original-merge: caa05c1bf2c55053a65091886f0ddf78865fc72c Merged-by: Bond-009 Backported-by: Bond_009 --- 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(); } /// From d8543351e256fef6f71ab2a4a45fcc50cfc4084c Mon Sep 17 00:00:00 2001 From: dfederm Date: Sat, 14 Feb 2026 05:57:24 -0500 Subject: [PATCH 298/300] Backport pull request #16226 from jellyfin/release-10.11.z Deduplicate provider IDs during MigrateLibraryDb migration Original-merge: 58c330b63d5e373ed1d9260a75d837922ca68830 Merged-by: Bond-009 Backported-by: Bond_009 --- 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 Date: Sat, 14 Feb 2026 05:57:25 -0500 Subject: [PATCH 299/300] 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 Backported-by: Bond_009 --- 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(); // 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 300/300] Backport pull request #16231 from jellyfin/release-10.11.z Skip image checks for empty folders Original-merge: 8cd3090ceedbefd680b26dc28266ae9a6e5d652a Merged-by: Bond-009 Backported-by: Bond_009 --- .../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;