From 9ec657cf6e93dcf26206a2be98c2072bb63d6d23 Mon Sep 17 00:00:00 2001 From: felix920506 Date: Wed, 27 Mar 2024 13:23:10 -0400 Subject: [PATCH 01/44] Remove permission check from GHA Permission check is moved to the script itself --- .github/workflows/commands.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index d78f11166c..23636cff64 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -124,7 +124,7 @@ jobs: rename: name: Rename - if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' + if: contains(github.event.comment.body, '@jellyfin-bot rename') runs-on: ubuntu-latest steps: - name: pull in script From c46dff16ccceffcc67fbcc505790867d4c454a6e Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Sun, 11 Dec 2022 14:12:23 -0600 Subject: [PATCH 02/44] Set IsLive --- src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index 7dc30f7275..88ec0b14e6 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -170,6 +170,7 @@ namespace Jellyfin.LiveTv.Listings IsSeries = program.Episode.Episode is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, + IsLive = program.IsLive, IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), From 80365f277afa05b329422362cbef6a4aa92317cd Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 4 Mar 2024 00:28:41 -0500 Subject: [PATCH 03/44] chore(ci): Add permissions grant Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index f7366c7e04..96e72743e6 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -8,6 +8,10 @@ on: schedule: - cron: '24 2 * * 4' +permissions: + contents: read + security-events: write + jobs: analyze: name: Analyze From 47af1c4576dbfbfd438bba248b582d1f312a84cd Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 15:31:52 +0800 Subject: [PATCH 04/44] Don't allow library name with leading or trailing space Windows has a specific requirement on filenames that cannot have leading or trailing spaces and the folder creation would fail. Reject such request in the api controller. --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 55000fc91e..eb2aba8814 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -81,6 +82,12 @@ public class LibraryStructureController : BaseJellyfinApiController [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { + // Windows does not allow files or folders with names that has leading or trailing spaces + if (name.Length != name.Trim().Length) + { + return BadRequest(); + } + var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); if (paths is not null && paths.Length > 0) From 2904083053bdad516de07324b497ed268519fab1 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 15:34:04 +0800 Subject: [PATCH 05/44] Remove unused import --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index eb2aba8814..62193b1b4e 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; From b13039f377e52f5378773388ed33b6fff2d2ce50 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 16:10:25 +0800 Subject: [PATCH 06/44] Use regex attribute to validate input --- Jellyfin.Api/Controllers/LibraryStructureController.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 62193b1b4e..e3cb2cd9c0 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -75,18 +75,14 @@ public class LibraryStructureController : BaseJellyfinApiController [HttpPost] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task AddVirtualFolder( - [FromQuery] string name, + [FromQuery] + [RegularExpression(@"^(?:\S(?:.*\S)?)$", ErrorMessage = "Library name cannot be empty or have leading/trailing spaces.")] + string name, [FromQuery] CollectionTypeOptions? collectionType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths, [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { - // Windows does not allow files or folders with names that has leading or trailing spaces - if (name.Length != name.Trim().Length) - { - return BadRequest(); - } - var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); if (paths is not null && paths.Length > 0) From 43a055d7ea773b0e13c2dc66a0b4aa93df873f91 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Sun, 2 Nov 2025 11:11:39 -0800 Subject: [PATCH 07/44] Enable jellyfin.db customized path This enables moving where the database is stored to another directory that doesn't have all the trickplay/subtitles/etc Fixes #15354 --- .../SqliteDatabaseProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 2b000b257b..7438d8d458 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -61,7 +61,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var customOptions = databaseConfiguration.CustomProviderOptions?.Options; var sqliteConnectionBuilder = new SqliteConnectionStringBuilder(); - sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); + sqliteConnectionBuilder.DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db")); sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse, () => SqliteCacheMode.Default); sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true); From 37983c943a7f65850cc2bd4854f22dbe2ae92c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Nie=C5=82acny?= Date: Fri, 13 Mar 2026 16:34:44 +0100 Subject: [PATCH 08/44] Respect EnableSubtitleExtraction setting in subtitle delivery Wire up EnableSubtitleExtraction config to MediaEncoder.CanExtractSubtitles so the setting is actually respected. Gate subtitle extraction check behind PlayMethod.Transcode since DirectPlay has no competing ffmpeg process. Add parameterized tests for StreamBuilder.GetSubtitleProfile covering text and graphical codecs, profile format matching, and extraction setting behavior. Remove misplaced SubtitleEncoder extraction test. --- .../Encoder/MediaEncoder.cs | 3 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- .../Dlna/StreamBuilderTests.cs | 55 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 73c5b88c8b..770965cab3 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1331,8 +1331,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public bool CanExtractSubtitles(string codec) { - // TODO is there ever a case when a subtitle can't be extracted?? - return true; + return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction; } private sealed class ProcessWrapper : IDisposable diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 42cb208d08..166d574d25 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1555,7 +1555,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) + if (!subtitleStream.IsExternal && playMethod == PlayMethod.Transcode && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) { continue; } diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 2c1080ffe3..8269ae58cd 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests return (path, query, filename, extension); } + + [Theory] + // EnableSubtitleExtraction = false, internal subtitles + [InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + [InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + [InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + // EnableSubtitleExtraction = false, external subtitles + [InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + // EnableSubtitleExtraction = true, internal subtitles + [InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + // EnableSubtitleExtraction = true, external subtitles + [InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + public void GetSubtitleProfile_RespectsExtractionSetting( + string codec, + string profileFormat, + bool enableSubtitleExtraction, + bool isExternal, + PlayMethod playMethod, + SubtitleDeliveryMethod expectedMethod) + { + var mediaSource = new MediaSourceInfo(); + var subtitleStream = new MediaStream + { + Type = MediaStreamType.Subtitle, + Index = 0, + IsExternal = isExternal, + Path = isExternal ? "/media/sub." + codec : null, + Codec = codec, + SupportsExternalStream = MediaStream.IsTextFormat(codec) + }; + + var subtitleProfiles = new[] + { + new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External } + }; + + var transcoderSupport = new Mock(); + transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny())).Returns(enableSubtitleExtraction); + + var result = StreamBuilder.GetSubtitleProfile( + mediaSource, + subtitleStream, + subtitleProfiles, + playMethod, + transcoderSupport.Object, + null, + null); + + Assert.Equal(expectedMethod, result.Method); + } } } From 300036c85913b79bf9bbf13c81ea2241f1216f78 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 23 Mar 2026 17:08:15 -0400 Subject: [PATCH 09/44] Fix FolderStorageInfo to show parent filesystem A direct implementation using DriveInfo directly on a path does not work as expected. The method will return a DriveInfo object with the given path as both the Name and the RootDirectory, which is not helpful. Instead, add parsing logic to find the best possible match out of all filesystems on the system for the path, including handling edge cases involving symlinked paths in the chain. This ensures that the resulting DeviceId is a valid filesystem, allowing it to be used in the UI to show a better description. It also includes the new ResolvedPath which will show, if required, what the Path resolved to after all symlinks are interpolated. One possible issue here is that walking all drives as-is might become slow(er) on a system with many partitions, but even on my partition-heavy system with over a dozen ZVOLs and remote mounts, this takes under 0.4 seconds including runup time for `dotnet run`, so I imagine this should be fine. --- .../StorageHelpers/StorageHelper.cs | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index ce628a04d0..0b8e2830d2 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -28,22 +28,41 @@ public static class StorageHelper } /// - /// Gets the free space of a specific directory. + /// Gets the free space of the parent filesystem of a specific directory. /// /// Path to a folder. - /// The number of bytes available space. + /// Various details about the parent filesystem containing the directory. public static FolderStorageInfo GetFreeSpaceOf(string path) { try { - var driveInfo = new DriveInfo(path); + // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar. + resolvedPath = ResolvePath(path); + // We iterate all filesystems reported by GetDrives() here, and attempt to find the best + // match that contains, as deep as possible, the given path. + // This is required because simply calling `DriveInfo` on a path returns that path as + // the Name and RootDevice, which is not at all how this should work. + DriveInfo[] allDrives = DriveInfo.GetDrives(); + DriveInfo bestMatch = null; + foreach (DriveInfo d in allDrives) + { + if (resolvedPath.StartsWith(d.RootDirectory.FullName) && + (bestMatch == null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length)) + { + bestMatch = d; + } + } + if (bestMatch is null) { + throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid."); + } return new FolderStorageInfo() { Path = path, - FreeSpace = driveInfo.AvailableFreeSpace, - UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace, - StorageType = driveInfo.DriveType.ToString(), - DeviceId = driveInfo.Name, + ResolvedPath = resolvedPath, + FreeSpace = bestMatch.AvailableFreeSpace, + UsedSpace = bestMatch.TotalSize - bestMatch.AvailableFreeSpace, + StorageType = bestMatch.DriveType.ToString(), + DeviceId = bestMatch.Name, }; } catch @@ -59,6 +78,26 @@ public static class StorageHelper } } + /// + /// Walk a path and fully resolve any symlinks within it. + /// + private static string ResolvePath(string path) + { + var parts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + var current = Path.DirectorySeparatorChar.ToString(); + foreach (var part in parts) + { + current = Path.Combine(current, part); + var resolved = new DirectoryInfo(current).ResolveLinkTarget(returnFinalTarget: true); + if (resolved is not null) + { + current = resolved.FullName; + } + } + + return current; + } + /// /// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold. /// From 434ebc8b110a2736c9be08360c17cf74e27803d1 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 23 Mar 2026 17:11:29 -0400 Subject: [PATCH 10/44] Ensure ResolvedPath is sent on error too --- Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index 0b8e2830d2..a36a51330d 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -70,6 +70,7 @@ public static class StorageHelper return new FolderStorageInfo() { Path = path, + ResolvedPath = path, FreeSpace = -1, UsedSpace = -1, StorageType = null, From 418beafebb49527974c5563907367e6b689352a3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 23 Mar 2026 17:15:49 -0400 Subject: [PATCH 11/44] Update FolderStorageInfo record --- MediaBrowser.Model/System/FolderStorageInfo.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs index 7b10e4ea58..66356c6c48 100644 --- a/MediaBrowser.Model/System/FolderStorageInfo.cs +++ b/MediaBrowser.Model/System/FolderStorageInfo.cs @@ -11,17 +11,22 @@ public record FolderStorageInfo public required string Path { get; init; } /// - /// Gets the free space of the underlying storage device of the . + /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present) + /// + public required string ResolvedPath { get; init; } + + /// + /// Gets the free space of the underlying storage device of the . /// public long FreeSpace { get; init; } /// - /// Gets the used space of the underlying storage device of the . + /// Gets the used space of the underlying storage device of the . /// public long UsedSpace { get; init; } /// - /// Gets the kind of storage device of the . + /// Gets the kind of storage device of the . /// public string? StorageType { get; init; } From 8142bbd50e4c2218e99c621900430b0189c267c3 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 23 Mar 2026 17:22:35 -0400 Subject: [PATCH 12/44] Properly define variable type --- Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index a36a51330d..b80d65ecbe 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -37,7 +37,7 @@ public static class StorageHelper try { // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar. - resolvedPath = ResolvePath(path); + string resolvedPath = ResolvePath(path); // We iterate all filesystems reported by GetDrives() here, and attempt to find the best // match that contains, as deep as possible, the given path. // This is required because simply calling `DriveInfo` on a path returns that path as From 965b602c6890623130d1d7e27de52e161c6d1069 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Mon, 23 Mar 2026 23:09:56 -0400 Subject: [PATCH 13/44] Apply suggestions from code review Co-authored-by: JPVenson --- .../StorageHelpers/StorageHelper.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index b80d65ecbe..d3f94ad0bd 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -37,24 +37,26 @@ public static class StorageHelper try { // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar. - string resolvedPath = ResolvePath(path); + var resolvedPath = ResolvePath(path); // We iterate all filesystems reported by GetDrives() here, and attempt to find the best // match that contains, as deep as possible, the given path. // This is required because simply calling `DriveInfo` on a path returns that path as // the Name and RootDevice, which is not at all how this should work. - DriveInfo[] allDrives = DriveInfo.GetDrives(); - DriveInfo bestMatch = null; + var allDrives = DriveInfo.GetDrives(); + DriveInfo? bestMatch = null; foreach (DriveInfo d in allDrives) { - if (resolvedPath.StartsWith(d.RootDirectory.FullName) && - (bestMatch == null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length)) + if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) && + (bestMatch is null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length)) { bestMatch = d; } } + if (bestMatch is null) { throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid."); } + return new FolderStorageInfo() { Path = path, From c22933260b1d9b8cd97980c00a70f53bbaaf4f54 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 24 Mar 2026 22:22:52 -0400 Subject: [PATCH 14/44] Fix linting issue --- MediaBrowser.Model/System/FolderStorageInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs index 66356c6c48..ebca39228b 100644 --- a/MediaBrowser.Model/System/FolderStorageInfo.cs +++ b/MediaBrowser.Model/System/FolderStorageInfo.cs @@ -11,7 +11,7 @@ public record FolderStorageInfo public required string Path { get; init; } /// - /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present) + /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present). /// public required string ResolvedPath { get; init; } From fec78c8448bd19f96460d853732cf24812443b70 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 24 Mar 2026 22:31:17 -0400 Subject: [PATCH 15/44] Lint for the Linter Gods --- .../StorageHelpers/StorageHelper.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index d3f94ad0bd..13c7895f83 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -43,7 +43,7 @@ public static class StorageHelper // This is required because simply calling `DriveInfo` on a path returns that path as // the Name and RootDevice, which is not at all how this should work. var allDrives = DriveInfo.GetDrives(); - DriveInfo? bestMatch = null; + DriveInfo? bestMatch = null; foreach (DriveInfo d in allDrives) { if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) && @@ -52,11 +52,12 @@ public static class StorageHelper bestMatch = d; } } - - if (bestMatch is null) { + + if (bestMatch is null) + { throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid."); } - + return new FolderStorageInfo() { Path = path, From 78e0a2b5c13eba9c65779bde41362fe728d57fb2 Mon Sep 17 00:00:00 2001 From: nour ovendodger Date: Tue, 7 Apr 2026 19:39:13 -0400 Subject: [PATCH 16/44] Translated using Weblate (Abkhazian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ab/ --- Emby.Server.Implementations/Localization/Core/ab.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json index bc6062f429..d6d257c5ba 100644 --- a/Emby.Server.Implementations/Localization/Core/ab.json +++ b/Emby.Server.Implementations/Localization/Core/ab.json @@ -1,3 +1,5 @@ { - "Albums": "аальбомқәа" + "Albums": "аальбомқәа", + "AppDeviceValues": "Апп: {0}, Априбор: {1}", + "Application": "Апрограмма" } From 68f26e5a344bfa9f8f26c02acf1ed2ccb8913712 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:37:03 +0000 Subject: [PATCH 17/44] Update dependency Microsoft.NET.Test.Sdk to 18.4.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f15f7c7a75..bf79ff0e0e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ - + From 553f38a2377cf843404cd4d3b3602e8a72bc75f8 Mon Sep 17 00:00:00 2001 From: Lasath Fernando Date: Sat, 4 Apr 2026 16:10:07 +0000 Subject: [PATCH 18/44] Fix language display for ISO 639-2-only codes (e.g. mul, und) LoadCultures() in LocalizationManager skipped all iso6392.txt entries without a two-letter ISO 639-1 code, dropping 302 of 496 languages including mul (Multiple languages), und (Undetermined), mis (Uncoded languages), zxx, and many real languages like Achinese, Akkadian, etc. This caused FindLanguageInfo() to return null for these codes, which meant: - ExternalPathParser could not recognize them as valid language codes in subtitle filenames, so the Language field was never set - DisplayTitle fell back to the raw code string (e.g. "Mul") Fix by allowing entries without two-letter codes to be loaded with an empty TwoLetterISOLanguageName. Also set LocalizedLanguage in ProbeResultNormalizer for ffprobe-detected streams (the DB repository path was already handled on master). --- .../Localization/LocalizationManager.cs | 2 +- .../Probing/ProbeResultNormalizer.cs | 6 ++++++ MediaBrowser.Model/Entities/MediaStream.cs | 1 - .../Localization/LocalizationManagerTests.cs | 21 ++++++++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index bc80c2b405..6fca5bc1ba 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization string twoCharName = parts[2]; if (string.IsNullOrWhiteSpace(twoCharName)) { - continue; + twoCharName = string.Empty; } else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index d3e7b52315..203e72de36 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,6 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); + stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) + ? _localization.FindLanguageInfo(stream.Language)?.DisplayName + : null; stream.Channels = streamInfo.Channels; @@ -767,6 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); + stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) + ? _localization.FindLanguageInfo(stream.Language)?.DisplayName + : null; if (string.IsNullOrEmpty(stream.Title)) { diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 11f81ff7d8..4491fb5ace 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Text; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index e60522bf78..700ac5dced 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var cultures = localizationManager.GetCultures().ToList(); - Assert.Equal(194, cultures.Count); + Assert.Equal(496, cultures.Count); var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal)); Assert.NotNull(germany); @@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Contains("ger", germany.ThreeLetterISOLanguageNames); } + [Theory] + [InlineData("mul", "Multiple languages")] + [InlineData("und", "Undetermined")] + [InlineData("mis", "Uncoded languages")] + [InlineData("zxx", "No linguistic content; Not applicable")] + public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName) + { + var localizationManager = Setup(new ServerConfiguration + { + UICulture = "en-US" + }); + await localizationManager.LoadAll(); + + var culture = localizationManager.FindLanguageInfo(code); + Assert.NotNull(culture); + Assert.Equal(expectedDisplayName, culture.DisplayName); + Assert.Equal(code, culture.ThreeLetterISOLanguageName); + } + [Fact] public async Task GetParentalRatings_Default_Success() { From c300651d0ddae2e2a3b3cde55989284f5972a79a Mon Sep 17 00:00:00 2001 From: Lasath Fernando Date: Thu, 9 Apr 2026 13:53:38 -0500 Subject: [PATCH 19/44] Simplify null-check ternary style in ProbeResultNormalizer Co-authored-by: Bond-009 --- .../Probing/ProbeResultNormalizer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 203e72de36..3c6a03713f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,9 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); - stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) - ? _localization.FindLanguageInfo(stream.Language)?.DisplayName - : null; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; stream.Channels = streamInfo.Channels; @@ -770,9 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); - stream.LocalizedLanguage = !string.IsNullOrEmpty(stream.Language) - ? _localization.FindLanguageInfo(stream.Language)?.DisplayName - : null; + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; if (string.IsNullOrEmpty(stream.Title)) { From 22f050725878661d475b63745656a5b9d57dafe5 Mon Sep 17 00:00:00 2001 From: Dominic DeMarco Date: Fri, 10 Apr 2026 00:21:52 -0700 Subject: [PATCH 20/44] Record missing information Fixes tonemapping checks by recording previously missing information when gathering video stream information from videos in a BDMV structure --- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index bde23e842f..a89f059060 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -366,6 +366,8 @@ namespace MediaBrowser.Providers.MediaInfo blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace; blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer; blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries; + blurayVideoStream.BitDepth = ffmpegVideoStream.BitDepth; + blurayVideoStream.PixelFormat = ffmpegVideoStream.PixelFormat; } } From 29d11f6ecb529ca091f6538ea3dc6c9a0fd6d4c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:09:15 +0000 Subject: [PATCH 21/44] Update actions/upload-artifact action to v7.0.1 --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/openapi-generate.yml | 2 +- .github/workflows/openapi-pull-request.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index f9e2fbc3a6..dd48209a1f 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: abi-base retention-days: 14 diff --git a/.github/workflows/openapi-generate.yml b/.github/workflows/openapi-generate.yml index 255cc49e82..dbfaf9d30b 100644 --- a/.github/workflows/openapi-generate.yml +++ b/.github/workflows/openapi-generate.yml @@ -36,7 +36,7 @@ jobs: run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests - name: Upload Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.artifact }} path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index 563a0a406f..4acd0f4d4f 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -74,7 +74,7 @@ jobs: docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md - name: Upload Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: openapi-report path: /tmp/openapi-report/openapi-report.md From 6fc406f2c58e9dafc10f28d9c0e4eeb68283e26c Mon Sep 17 00:00:00 2001 From: Lofuuzi Date: Sat, 11 Apr 2026 19:22:03 -0400 Subject: [PATCH 22/44] Translated using Weblate (Chinese (Traditional Han script, Hong Kong)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant_HK/ --- Emby.Server.Implementations/Localization/Core/zh-HK.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 8ae899a73c..0a454b2938 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -29,7 +29,7 @@ "Inherit": "繼承", "ItemAddedWithName": "{0} 經已加咗入媒體櫃", "ItemRemovedWithName": "{0} 經已由媒體櫃移除咗", - "LabelIpAddressValue": "IP 地址:{0}", + "LabelIpAddressValue": "IP 位址:{0}", "LabelRunningTimeValue": "運行時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 經已更新咗", From 22644075e784ccd71d39a27eae6d1f7434f47a00 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 12 Apr 2026 12:42:49 +0900 Subject: [PATCH 23/44] add NameStartsWith and NameLessThan filters to Person search --- Jellyfin.Api/Controllers/PersonsController.cs | 6 ++++++ .../Item/PeopleRepository.cs | 10 ++++++++++ .../Entities/InternalPeopleQuery.cs | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 2b2afb0fe6..a113ded049 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -50,6 +50,8 @@ public class PersonsController : BaseJellyfinApiController /// Optional. All items with a lower index will be dropped from the response. /// Optional. The maximum number of records to return. /// The search term. + /// Optional. Filter by items whose name starts with the given input string. + /// Optional. Filter by items whose name will appear before this value when sorted alphabetically. /// Optional. Specify additional fields of information to return in the output. /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. userId is required. @@ -70,6 +72,8 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, @@ -97,6 +101,8 @@ public class PersonsController : BaseJellyfinApiController excludePersonTypes) { NameContains = searchTerm, + NameStartsWith = nameStartsWith, + NameLessThan = nameLessThan, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index ad9953d1b6..adb5e08cf9 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -235,6 +235,16 @@ public class PeopleRepository(IDbContextFactory dbProvider, I query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper)); } + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + { + query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant())); + } + + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) + { + query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0); + } + return query; } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index f4b3910b0e..373ec7ffeb 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -42,6 +42,10 @@ namespace MediaBrowser.Controller.Entities public string NameContains { get; set; } + public string NameStartsWith { get; set; } + + public string NameLessThan { get; set; } + public User User { get; set; } public bool? IsFavorite { get; set; } From bb265cd4030e966aec1afbd31eafc0973531c232 Mon Sep 17 00:00:00 2001 From: dkanada Date: Mon, 13 Apr 2026 13:50:04 +0900 Subject: [PATCH 24/44] add NameStartsWithOrGreater parameter to Persons endpoint --- Jellyfin.Api/Controllers/PersonsController.cs | 3 +++ Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 5 +++++ MediaBrowser.Controller/Entities/InternalPeopleQuery.cs | 2 ++ 3 files changed, 10 insertions(+) diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index a113ded049..1811a219ac 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -52,6 +52,7 @@ public class PersonsController : BaseJellyfinApiController /// The search term. /// Optional. Filter by items whose name starts with the given input string. /// Optional. Filter by items whose name will appear before this value when sorted alphabetically. + /// Optional. Filter by items whose name will appear after this value when sorted alphabetically. /// Optional. Specify additional fields of information to return in the output. /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. userId is required. @@ -74,6 +75,7 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] string? searchTerm, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, + [FromQuery] string? nameStartsWithOrGreater, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, @@ -103,6 +105,7 @@ public class PersonsController : BaseJellyfinApiController NameContains = searchTerm, NameStartsWith = nameStartsWith, NameLessThan = nameLessThan, + NameStartsWithOrGreater = nameStartsWithOrGreater, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index adb5e08cf9..7147fbfe7d 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -245,6 +245,11 @@ public class PeopleRepository(IDbContextFactory dbProvider, I query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0); } + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) + { + query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0); + } + return query; } diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 373ec7ffeb..e12ba22343 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -46,6 +46,8 @@ namespace MediaBrowser.Controller.Entities public string NameLessThan { get; set; } + public string NameStartsWithOrGreater { get; set; } + public User User { get; set; } public bool? IsFavorite { get; set; } From ce3fa80a28a660fbfed632b6e556c2a77c7b0620 Mon Sep 17 00:00:00 2001 From: Gargotaire Date: Mon, 13 Apr 2026 13:15:41 -0400 Subject: [PATCH 25/44] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index fce3a614c2..ec5cbf0d43 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -63,8 +63,8 @@ "Photos": "Fotos", "Playlists": "Llistes de reproducció", "Plugin": "Complement", - "PluginInstalledWithName": "{0} s'ha instal·lat", - "PluginUninstalledWithName": "{0} s'ha desinstal·lat", + "PluginInstalledWithName": "S'ha instal·lat {0}", + "PluginUninstalledWithName": "S'ha desinstal·lat {0}", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", From fb33b725e064f0954c5a4f2af17041b7552eb6e1 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 13 Apr 2026 20:06:46 +0200 Subject: [PATCH 26/44] Fix in-process restart (#16482) Fix in-process restart --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 1 + Jellyfin.Server/Program.cs | 3 ++- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 7cff2a25b6..23bd5cf200 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -60,6 +60,7 @@ namespace Emby.Server.Implementations.IO _fileSystem = fileSystem; appLifetime.ApplicationStarted.Register(Start); + appLifetime.ApplicationStopping.Register(Stop); } /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 93f71fdc69..93ba672535 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -161,7 +161,6 @@ namespace Jellyfin.Server _loggerFactory, options, startupConfig); - _appHost = appHost; var configurationCompleted = false; try { @@ -207,6 +206,7 @@ namespace Jellyfin.Server await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false); await appHost.InitializeServices(startupConfig).ConfigureAwait(false); + _appHost = appHost; await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.AppInitialisation, appHost.ServiceProvider).ConfigureAwait(false); await jellyfinMigrationService.CleanupSystemAfterMigration(_logger).ConfigureAwait(false); @@ -263,6 +263,7 @@ namespace Jellyfin.Server _appHost = null; _jellyfinHost?.Dispose(); + _jellyfinHost = null; } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 1aa39f97b6..05975929db 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -142,6 +142,7 @@ public sealed class SetupServer : IDisposable ThrowIfDisposed(); var retryAfterValue = TimeSpan.FromSeconds(5); var config = _configurationManager.GetNetworkConfiguration()!; + _startupServer?.Dispose(); _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"]) .UseConsoleLifetime() .UseSerilog() From 5bad7b8ae324ccf0d4eb3051d0944edfddfdb50b Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 14 Apr 2026 12:38:01 -0400 Subject: [PATCH 27/44] Fix artist metadata not being fetched on initial library scan (#16606) * Fix artist metadata not being fetched on initial library scan * Update Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs Co-authored-by: Bond-009 --------- Co-authored-by: Bond-009 --- .../Library/Validators/ArtistsValidator.cs | 11 ++++++++++- MediaBrowser.Providers/Manager/ProviderManager.cs | 7 +++++++ .../Plugins/AudioDb/AudioDbArtistProvider.cs | 6 +++++- .../Plugins/MusicBrainz/MusicBrainzArtistProvider.cs | 6 +++++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7cc851b73b..ef20ae9bca 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -50,6 +50,10 @@ public class ArtistsValidator public async Task Run(IProgress progress, CancellationToken cancellationToken) { var names = _itemRepo.GetAllArtistNames(); + var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist] + }).ToHashSet(); var numComplete = 0; var count = names.Count; @@ -59,8 +63,13 @@ public class ArtistsValidator try { var item = _libraryManager.GetArtist(name); + var isNew = !existingArtistIds.Contains(item.Id); + var neverRefreshed = item.DateLastRefreshed == default; - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + if (isNew || neverRefreshed) + { + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException) { diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f8e2aece1f..0bab73180f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -487,6 +487,13 @@ namespace MediaBrowser.Providers.Manager return true; } + // Artists without a folder structure that are derived from metadata have no real path in the library, + // so GetLibraryOptions returns null. Allow all providers through rather than blocking them. + if (item is MusicArtist && libraryTypeOptions is null) + { + return true; + } + return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 00bd96282c..d8cb6b4b24 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -125,7 +125,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (string.IsNullOrWhiteSpace(overview)) { - overview = result.strBiographyEN; + overview = string.IsNullOrWhiteSpace(result.strBiographyEN) + ? result.strBiography + : result.strBiographyEN; } item.Overview = (overview ?? string.Empty).StripHtml(); @@ -224,6 +226,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string strTwitter { get; set; } + public string strBiography { get; set; } + public string strBiographyEN { get; set; } public string strBiographyDE { get; set; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 1323d2604a..9df21596c5 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// /// MusicBrainz artist provider. /// -public class MusicBrainzArtistProvider : IRemoteMetadataProvider, IDisposable +public class MusicBrainzArtistProvider : IRemoteMetadataProvider, IDisposable, IHasOrder { private readonly ILogger _logger; private Query _musicBrainzQuery; @@ -42,6 +42,10 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider public string Name => "MusicBrainz"; + /// + /// Runs first to populate the MusicBrainz artist ID used by downstream providers. + public int Order => 0; + private void ReloadConfig(object? sender, BasePluginConfiguration e) { var configuration = (PluginConfiguration)e; From 1ac5f71bf3cecce91c05f98b338d136833c1b672 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:21:13 +0000 Subject: [PATCH 28/44] Update dependency dotnet-ef to v10.0.6 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9cd9c08e75..3f7a9454c5 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "10.0.5", + "version": "10.0.6", "commands": [ "dotnet-ef" ] From f9c7b18fdd8f7dfd46e642d0a829436fd352d6db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:21:22 +0000 Subject: [PATCH 29/44] Update Microsoft to 10.0.6 --- Directory.Packages.props | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bf79ff0e0e..9b0d6f93c5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,27 +26,27 @@ - - + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -77,7 +77,7 @@ - + From fa8073d9ff7fbfea29970fd1762f4723f6ee556b Mon Sep 17 00:00:00 2001 From: Sakari Kukkonen Date: Tue, 14 Apr 2026 19:10:10 -0400 Subject: [PATCH 30/44] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- Emby.Server.Implementations/Localization/Core/fi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 15a04d22cd..79afbb519b 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -39,8 +39,8 @@ "Channels": "Kanavat", "CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}", "Books": "Kirjat", - "AuthenticationSucceededWithUserName": "{0} on todennettu", - "Artists": "Esittäjät", + "AuthenticationSucceededWithUserName": "{0} todennus onnistunut", + "Artists": "Artistit", "Application": "Sovellus", "AppDeviceValues": "Sovellus: {0}, Laite: {1}", "Albums": "Albumit", From 03523be5555ba76171dafe367d0606570424a2f6 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 15 Apr 2026 09:06:12 -0400 Subject: [PATCH 31/44] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 94db435715..37a1e70240 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "Omiljene serije", "HeaderFavoriteSongs": "Omiljene pjesme", "HeaderLiveTV": "TV uživo", - "HeaderNextUp": "Slijedi", + "HeaderNextUp": "Sljedeće na redu", "HeaderRecordingGroups": "Grupa snimka", "HomeVideos": "Kućni video", "Inherit": "Naslijedi", @@ -73,10 +73,10 @@ "Shows": "Emisije", "Songs": "Pjesme", "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", - "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", + "SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}", "Sync": "Sinkronizacija", "System": "Sustav", - "TvShows": "Serije", + "TvShows": "TV emisije", "User": "Korisnik", "UserCreatedWithName": "Korisnik {0} je kreiran", "UserDeletedWithName": "Korisnik {0} je obrisan", @@ -99,8 +99,8 @@ "TaskCleanCache": "Očisti mapu predmemorije", "TasksApplicationCategory": "Aplikacija", "TasksMaintenanceCategory": "Održavanje", - "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.", - "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje", + "TaskDownloadMissingSubtitlesDescription": "Pretraži internet za nedsotajućim titlovima ne osnovi konfiguracije metapodataka.", + "TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove", "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.", "TaskRefreshChannels": "Osvježi kanale", "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.", From ac6edd40c8fcccbc05ca47aceed815795a8b8283 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 15 Apr 2026 09:44:43 -0400 Subject: [PATCH 32/44] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 37a1e70240..c748d68a4d 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -89,7 +89,7 @@ "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", - "ValueSpecialEpisodeName": "Posebno - {0}", + "ValueSpecialEpisodeName": "Posebno – {0}", "VersionNumber": "Verzija {0}", "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.", "TaskRefreshLibrary": "Skeniraj medijsku biblioteku", From 1864c174128052b1acdbe3dc132c56c619b88fed Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 15 Apr 2026 10:20:46 -0400 Subject: [PATCH 33/44] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index c748d68a4d..b1d3548e9b 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -119,7 +119,7 @@ "Forced": "Forsirani", "Default": "Zadano", "TaskOptimizeDatabase": "Optimiziraj bazu podataka", - "External": "Vanjski", + "External": "Eksterni", "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "TaskKeyframeExtractor": "Izvoditelj ključnog okvira", "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.", From 8a01f04de50970f205462e6ea1bfc96fc7573d0e Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 15 Apr 2026 10:26:32 -0400 Subject: [PATCH 34/44] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index b1d3548e9b..289145f670 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -88,11 +88,11 @@ "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}", "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}", - "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", + "ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija", "ValueSpecialEpisodeName": "Posebno – {0}", "VersionNumber": "Verzija {0}", - "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.", - "TaskRefreshLibrary": "Skeniraj medijsku biblioteku", + "TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.", + "TaskRefreshLibrary": "Skeniraj biblioteku medija", "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.", "TaskRefreshChapterImages": "Izdvoji slike poglavlja", "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.", @@ -107,7 +107,7 @@ "TaskCleanTranscode": "Očisti mapu transkodiranja", "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.", "TaskUpdatePlugins": "Ažuriraj dodatke", - "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.", + "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u biblioteci medija.", "TaskRefreshPeople": "Osvježi osobe", "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.", "TaskCleanLogs": "Očisti mapu dnevnika zapisa", From 9239b1211885d5d4b7fec0c9b61baac9ad4778e5 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 15 Apr 2026 10:27:45 -0400 Subject: [PATCH 35/44] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 289145f670..eaeb173c20 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -135,7 +135,7 @@ "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama", "TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.", "TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja", - "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.", + "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.", "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka", "CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana." } From 8ba9319f27bb6d2e93640b9c8426c4e6e9470133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hilmar=20G=C3=BAstafsson?= Date: Wed, 15 Apr 2026 19:11:29 +0200 Subject: [PATCH 36/44] fix: retain subtitles spanning HLS segment boundaries (#16594) fix: retain subtitles spanning HLS segment boundaries --- CONTRIBUTORS.md | 1 + .../Subtitles/SubtitleEncoder.cs | 10 +- .../Subtitles/FilterEventsTests.cs | 282 ++++++++++++++++++ 3 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b0cec3c92..01f968690f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -212,6 +212,7 @@ - [martenumberto](https://github.com/martenumberto) - [ZeusCraft10](https://github.com/ZeusCraft10) - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) + - [LiHRaM](https://github.com/LiHRaM) # Emby Contributors diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9aeac7221e..5920fe3289 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -101,11 +101,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles return ms; } - private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) + internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) { - // Drop subs that are earlier than what we're looking for + // Drop subs that have fully elapsed before the requested start position track.TrackEvents = track.TrackEvents - .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0) + .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0) .ToArray(); if (endTimeTicks > 0) @@ -119,8 +119,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles { foreach (var trackEvent in track.TrackEvents) { - trackEvent.EndPositionTicks -= startPositionTicks; - trackEvent.StartPositionTicks -= startPositionTicks; + trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks); + trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks); } } } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs new file mode 100644 index 0000000000..5f84e85592 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs @@ -0,0 +1,282 @@ +using System; +using AutoFixture; +using AutoFixture.AutoMoq; +using MediaBrowser.MediaEncoding.Subtitles; +using MediaBrowser.Model.MediaInfo; +using Xunit; + +namespace Jellyfin.MediaEncoding.Subtitles.Tests +{ + public class FilterEventsTests + { + private readonly SubtitleEncoder _encoder; + + public FilterEventsTests() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + _encoder = fixture.Create(); + } + + [Fact] + public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained() + { + // Subtitle starts at 5s, ends at 15s. + // Segment requested from 10s to 20s. + // The subtitle is still on screen at 10s and should NOT be dropped. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Still on screen") + { + StartPositionTicks = TimeSpan.FromSeconds(5).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("2", "Next subtitle") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(17).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Equal(2, track.TrackEvents.Count); + Assert.Equal("1", track.TrackEvents[0].Id); + Assert.Equal("2", track.TrackEvents[1].Id); + } + + [Fact] + public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped() + { + // Subtitle starts at 2s, ends at 5s. + // Segment requested from 10s. + // The subtitle ended before the segment — should be dropped. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Already gone") + { + StartPositionTicks = TimeSpan.FromSeconds(2).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(5).Ticks + }, + new SubtitleTrackEvent("2", "Visible") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(17).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Single(track.TrackEvents); + Assert.Equal("2", track.TrackEvents[0].Id); + } + + [Fact] + public void FilterEvents_SubtitleAfterSegment_IsDropped() + { + // Segment is 10s-20s, subtitle starts at 25s. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "In range") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("2", "After segment") + { + StartPositionTicks = TimeSpan.FromSeconds(25).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(30).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Single(track.TrackEvents); + Assert.Equal("1", track.TrackEvents[0].Id); + } + + [Fact] + public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps() + { + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Subtitle") + { + StartPositionTicks = TimeSpan.FromSeconds(15).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(20).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(30).Ticks, + preserveTimestamps: false); + + Assert.Single(track.TrackEvents); + // Timestamps should be shifted back by 10s + Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks); + Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks); + } + + [Fact] + public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps() + { + var startTicks = TimeSpan.FromSeconds(15).Ticks; + var endTicks = TimeSpan.FromSeconds(20).Ticks; + + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Subtitle") + { + StartPositionTicks = startTicks, + EndPositionTicks = endTicks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(30).Ticks, + preserveTimestamps: true); + + Assert.Single(track.TrackEvents); + Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks); + Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks); + } + + [Fact] + public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained() + { + // Subtitle ends exactly when the segment begins. + // EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0, + // so SkipWhile stops and the subtitle is retained. + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Boundary subtitle") + { + StartPositionTicks = TimeSpan.FromSeconds(5).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(10).Ticks + }, + new SubtitleTrackEvent("2", "In range") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: true); + + Assert.Equal(2, track.TrackEvents.Count); + Assert.Equal("1", track.TrackEvents[0].Id); + } + + [Fact] + public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps() + { + // Subtitle starts at 5s, ends at 15s. + // Segment requested from 10s to 20s, preserveTimestamps = false. + // The subtitle spans the boundary and is retained, but shifting + // StartPositionTicks by -10s would produce -5s (negative). + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Spans boundary") + { + StartPositionTicks = TimeSpan.FromSeconds(5).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("2", "Fully in range") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(17).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: TimeSpan.FromSeconds(20).Ticks, + preserveTimestamps: false); + + Assert.Equal(2, track.TrackEvents.Count); + // Subtitle 1: start should be clamped to 0, not -5s + Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative"); + Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks); + // Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s) + Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks); + Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks); + } + + [Fact] + public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition() + { + var track = new SubtitleTrackInfo + { + TrackEvents = new[] + { + new SubtitleTrackEvent("1", "Before") + { + StartPositionTicks = TimeSpan.FromSeconds(2).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(4).Ticks + }, + new SubtitleTrackEvent("2", "After") + { + StartPositionTicks = TimeSpan.FromSeconds(12).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(15).Ticks + }, + new SubtitleTrackEvent("3", "Much later") + { + StartPositionTicks = TimeSpan.FromSeconds(500).Ticks, + EndPositionTicks = TimeSpan.FromSeconds(505).Ticks + } + } + }; + + _encoder.FilterEvents( + track, + startPositionTicks: TimeSpan.FromSeconds(10).Ticks, + endTimeTicks: 0, + preserveTimestamps: true); + + Assert.Equal(2, track.TrackEvents.Count); + Assert.Equal("2", track.TrackEvents[0].Id); + Assert.Equal("3", track.TrackEvents[1].Id); + } + } +} From eb6c0e093552974d09476135ce95501ef01f5c46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:46:47 +0200 Subject: [PATCH 37/44] Update github/codeql-action action to v4.35.2 (#16639) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index f6f9a26d06..442114dd80 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -32,13 +32,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 From 8510cfe77de66d12bc88fcbef4b06ca637d90ab8 Mon Sep 17 00:00:00 2001 From: Eliya Wolfram Konzo Date: Wed, 15 Apr 2026 20:59:13 +0300 Subject: [PATCH 38/44] Added Tanzania countries.json (#14406) * Update countries.json Added Tanzania * Fix order countries.json * Fix tests --------- Co-authored-by: Bond_009 --- Emby.Server.Implementations/Localization/countries.json | 6 ++++++ .../Localization/LocalizationManagerTests.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index d92dc880b1..811a7d4094 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -749,6 +749,12 @@ "ThreeLetterISORegionName": "TAJ", "TwoLetterISORegionName": "TJ" }, + { + "DisplayName": "Tanzania", + "Name": "TZ", + "ThreeLetterISORegionName": "TZA", + "TwoLetterISORegionName": "TZ" + }, { "DisplayName": "Thailand", "Name": "TH", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 700ac5dced..5bcfc580ff 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization }); var countries = localizationManager.GetCountries().ToList(); - Assert.Equal(139, countries.Count); + Assert.Equal(140, countries.Count); var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal)); Assert.NotNull(germany); From d8c55a29bb0f3fe04ba083bb338ca80cf7f1601e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:59:35 +0000 Subject: [PATCH 39/44] Update danielpalme/ReportGenerator-GitHub-Action action to v5.5.5 --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index fc32cc884d..9fdcaedf97 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@cf6fe1b38ed5becc89ffe056c1f240825993be5b # v5.5.4 + uses: danielpalme/ReportGenerator-GitHub-Action@7ae927204961589fcb0b0be245c51fbbc87cbca2 # v5.5.5 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From 1e64e4284224949c3029ec8f1e3bc66e364da6dd Mon Sep 17 00:00:00 2001 From: SecularSteve Date: Fri, 17 Apr 2026 03:31:38 -0400 Subject: [PATCH 40/44] Added translation using Weblate (Bosnian) --- Emby.Server.Implementations/Localization/Core/bs.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/bs.json diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/bs.json @@ -0,0 +1 @@ +{} From 946ff12185289b5dc38732497bb362aab7f192fe Mon Sep 17 00:00:00 2001 From: SecularSteve Date: Fri, 17 Apr 2026 06:26:31 -0400 Subject: [PATCH 41/44] Translated using Weblate (Bosnian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/bs/ --- .../Localization/Core/bs.json | 142 +++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json index 0967ef424b..72b2a1f693 100644 --- a/Emby.Server.Implementations/Localization/Core/bs.json +++ b/Emby.Server.Implementations/Localization/Core/bs.json @@ -1 +1,141 @@ -{} +{ + "Albums": "Albumi", + "Artists": "Umjetnici", + "Books": "Knjige", + "Channels": "Kanalima", + "Collections": "Zbirke", + "Default": "Zadano", + "Favorites": "Omiljeni", + "Folders": "Mape", + "Genres": "Žanrovi", + "HeaderAlbumArtists": "Umjetnici albuma", + "HeaderContinueWatching": "Nastavi gledati", + "Movies": "Filmovi", + "MusicVideos": "Muzički spotovi", + "Photos": "Slike", + "Playlists": "Plejliste", + "Shows": "Pokazuje", + "Songs": "Pjesme", + "ValueSpecialEpisodeName": "Posebno - {0}", + "AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}", + "Application": "Prijava", + "AuthenticationSucceededWithUserName": "{0} uspješno autentificirano", + "CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}", + "ChapterNameValue": "Poglavlje {0}", + "DeviceOfflineWithName": "{0} se odspojio", + "DeviceOnlineWithName": "{0} je povezan", + "External": "Vanjsko", + "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}", + "Forced": "Prisilno", + "HeaderFavoriteAlbums": "Omiljeni albumi", + "HeaderFavoriteArtists": "Omiljeni umjetnici", + "HeaderFavoriteEpisodes": "Omiljene epizode", + "HeaderFavoriteShows": "Omiljene emisije", + "HeaderFavoriteSongs": "Omiljene pjesme", + "HeaderLiveTV": "TV uživo", + "HeaderNextUp": "Slijedi", + "HeaderRecordingGroups": "Grupe za snimanje", + "HearingImpaired": "Oštećen sluh", + "HomeVideos": "Kućni videozapisi", + "Inherit": "Nasljedi", + "ItemAddedWithName": "{0} je dodan u biblioteku", + "ItemRemovedWithName": "{0} je uklonjen iz biblioteke", + "LabelIpAddressValue": "IP adresa: {0}", + "LabelRunningTimeValue": "Trajanje: {0}", + "Latest": "Posljednje dodano", + "MessageApplicationUpdated": "Jellyfin Server je ažuriran", + "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana", + "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana", + "MixedContent": "Miješani sadržaj", + "Music": "Muzika", + "NameInstallFailed": "{0} instalacija je propala", + "NameSeasonNumber": "Sezona {0}", + "NameSeasonUnknown": "Sezona nepoznata", + "NewVersionIsAvailable": "Dostupna je nova verzija Jellyfin Servera za preuzimanje.", + "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", + "NotificationOptionApplicationUpdateInstalled": "Ažuriranje aplikacije instalirano", + "NotificationOptionAudioPlayback": "Pokrenuto je reproduciranje zvuka", + "NotificationOptionAudioPlaybackStopped": "Zaustavljeno je reproduciranje zvuka", + "NotificationOptionCameraImageUploaded": "Učitana slika s kamere", + "NotificationOptionInstallationFailed": "Neuspjeh instalacije", + "NotificationOptionNewLibraryContent": "Dodan novi sadržaj", + "NotificationOptionPluginError": "Neuspjeh dodatka", + "NotificationOptionPluginInstalled": "Dodatak je instaliran", + "NotificationOptionPluginUninstalled": "Dodatak je deinstaliran", + "NotificationOptionPluginUpdateInstalled": "Ažuriranje dodatka je instalirano", + "NotificationOptionServerRestartRequired": "Potreban je ponovni pokret servera", + "NotificationOptionTaskFailed": "Neuspjeh zakazane zadatke", + "NotificationOptionUserLockedOut": "Korisnik je zaključan", + "NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa", + "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena", + "Plugin": "Plugin", + "PluginInstalledWithName": "{0} je instaliran", + "PluginUninstalledWithName": "{0} je deinstaliran", + "PluginUpdatedWithName": "{0} je ažurirano", + "ProviderValue": "Pružatelj: {0}", + "ScheduledTaskFailedWithName": "{0} nije uspjelo", + "ScheduledTaskStartedWithName": "{0} počelo", + "ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti", + "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.", + "SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}", + "Sync": "Sinkronizacija", + "System": "Sistem", + "TvShows": "TV serije", + "Undefined": "Nedefinirano", + "User": "Korisnik", + "UserCreatedWithName": "Korisnik {0} je kreiran", + "UserDeletedWithName": "Korisnik {0} je izbrisan", + "UserDownloadingItemWithValues": "{0} preuzima {1}", + "UserLockedOutWithName": "Korisnik {0} je zaključan", + "UserOfflineFromDevice": "{0} se odspojio od {1}", + "UserOnlineFromDevice": "{0} je online od {1}", + "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", + "UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}", + "UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}", + "ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku", + "VersionNumber": "Verzija {0}", + "TasksMaintenanceCategory": "Održavanje", + "TasksLibraryCategory": "Biblioteka", + "TasksApplicationCategory": "Prijava", + "TasksChannelsCategory": "Internetski kanali", + "TaskCleanActivityLog": "Očisti dnevnik aktivnosti", + "TaskCleanActivityLogDescription": "Brisanje unosa u dnevnik aktivnosti starijih od konfigurisane starosti.", + "TaskCleanCache": "Očistite direktorij keša", + "TaskCleanCacheDescription": "Brisanje keš datoteka koje sistemu više nisu potrebne.", + "TaskRefreshChapterImages": "Izvadi slike iz poglavlja", + "TaskRefreshChapterImagesDescription": "Stvara minijature za videozapise koji imaju poglavlja.", + "TaskAudioNormalization": "Normalizacija zvuka", + "TaskAudioNormalizationDescription": "Skeneriše datoteke radi podataka za normalizaciju zvuka.", + "TaskRefreshLibrary": "Skenerisati medijsku biblioteku", + "TaskRefreshLibraryDescription": "Skenerira vašu medijsku biblioteku na nove datoteke i osvježava metapodatke.", + "TaskCleanLogs": "Očisti direktorij dnevnika", + "TaskCleanLogsDescription": "Brisanje dnevničkih datoteka starijih od {0} dana.", + "TaskRefreshPeople": "Osvježite ljude", + "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i režisere u vašoj medijskoj biblioteci.", + "TaskRefreshTrickplayImages": "Generirajte Trickplay slike", + "TaskRefreshTrickplayImagesDescription": "Stvara pregled trik-igara za videozapise u omogućenim bibliotekama.", + "TaskUpdatePlugins": "Ažuriraj dodatke", + "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja dodataka koji su konfigurisani da se automatski ažuriraju.", + "TaskCleanTranscode": "Očisti Transcode direktorij", + "TaskCleanTranscodeDescription": "Brisanje transkodiranih datoteka starijih od jednog dana.", + "TaskRefreshChannels": "Osvježi kanale", + "TaskRefreshChannelsDescription": "Osvježava informacije o internetskom kanalu.", + "TaskDownloadMissingLyrics": "Preuzmi nedostajuće tekstove", + "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama", + "TaskDownloadMissingSubtitles": "Preuzmite nedostajuće titlove", + "TaskDownloadMissingSubtitlesDescription": "Pretražuje internet u potrazi za nedostajućim titlovima na osnovu konfiguracije metapodataka.", + "TaskOptimizeDatabase": "Optimizirajte bazu podataka", + "TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.", + "TaskKeyframeExtractor": "Izvađač ključnih sličica", + "TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.", + "TaskCleanCollectionsAndPlaylists": "Očistite kolekcije i playliste", + "TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz kolekcija i playlista koje više ne postoje.", + "TaskExtractMediaSegments": "Analiza medijskog segmenta", + "TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.", + "TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay", + "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.", + "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka", + "CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana." +} From 7113a8c57ccf11930bc769a5a98c819c8e649f9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:00:44 +0000 Subject: [PATCH 42/44] Update dependency coverlet.collector to v10 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b0d6f93c5..34e6a02ff9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + From 1d44899606110ffd7d984bd7d52b8cd57e18c273 Mon Sep 17 00:00:00 2001 From: KGT1 Date: Sat, 18 Apr 2026 17:06:46 +0200 Subject: [PATCH 43/44] Remove global subtitle configuration (#14957) --- .../Providers/SubtitleConfigurationFactory.cs | 21 --------- .../Providers/SubtitleOptions.cs | 36 --------------- .../MediaInfo/FFProbeVideoInfo.cs | 41 ++--------------- .../MediaInfo/SubtitleScheduledTask.cs | 46 ++++++------------- 4 files changed, 20 insertions(+), 124 deletions(-) delete mode 100644 MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs delete mode 100644 MediaBrowser.Model/Providers/SubtitleOptions.cs diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs deleted file mode 100644 index 0445397ad8..0000000000 --- a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Providers; - -namespace MediaBrowser.Common.Providers -{ - public class SubtitleConfigurationFactory : IConfigurationFactory - { - /// - public IEnumerable GetConfigurations() - { - yield return new ConfigurationStore() - { - Key = "subtitles", - ConfigurationType = typeof(SubtitleOptions) - }; - } - } -} diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs deleted file mode 100644 index 6ea1e14862..0000000000 --- a/MediaBrowser.Model/Providers/SubtitleOptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Providers -{ - public class SubtitleOptions - { - public SubtitleOptions() - { - DownloadLanguages = Array.Empty(); - - SkipIfAudioTrackMatches = true; - RequirePerfectMatch = true; - } - - public bool SkipIfEmbeddedSubtitlesPresent { get; set; } - - public bool SkipIfAudioTrackMatches { get; set; } - - public string[] DownloadLanguages { get; set; } - - public bool DownloadMovieSubtitles { get; set; } - - public bool DownloadEpisodeSubtitles { get; set; } - - public string OpenSubtitlesUsername { get; set; } - - public string OpenSubtitlesPasswordHash { get; set; } - - public bool IsOpenSubtitleVipAccount { get; set; } - - public bool RequirePerfectMatch { get; set; } - } -} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index a89f059060..fdc2f36469 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Extensions; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -25,7 +24,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -74,7 +72,6 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleResolver = subtitleResolver; _mediaAttachmentRepository = mediaAttachmentRepository; _mediaStreamRepository = mediaStreamRepository; - _mediaStreamRepository = mediaStreamRepository; } public async Task ProbeVideo( @@ -551,47 +548,19 @@ namespace MediaBrowser.Providers.MediaInfo var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; - var subtitleOptions = _config.GetConfiguration("subtitles"); - var libraryOptions = _libraryManager.GetLibraryOptions(video); - string[] subtitleDownloadLanguages; - bool skipIfEmbeddedSubtitlesPresent; - bool skipIfAudioTrackMatches; - bool requirePerfectMatch; - bool enabled; - - if (libraryOptions.SubtitleDownloadLanguages is null) - { - subtitleDownloadLanguages = subtitleOptions.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; - requirePerfectMatch = subtitleOptions.RequirePerfectMatch; - enabled = (subtitleOptions.DownloadEpisodeSubtitles && - video is Episode) || - (subtitleOptions.DownloadMovieSubtitles && - video is Movie); - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; - enabled = true; - } - - if (enableSubtitleDownloading && enabled) + if (enableSubtitleDownloading && libraryOptions.SubtitleDownloadLanguages is not null) { var downloadedLanguages = await new SubtitleDownloader( _logger, _subtitleManager).DownloadSubtitles( video, currentStreams.Concat(externalSubtitleStreams).ToList(), - skipIfEmbeddedSubtitlesPresent, - skipIfAudioTrackMatches, - requirePerfectMatch, - subtitleDownloadLanguages, + libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent, + libraryOptions.SkipSubtitlesIfAudioTrackMatches, + libraryOptions.RequirePerfectSubtitleMatch, + libraryOptions.SubtitleDownloadLanguages, libraryOptions.DisabledSubtitleFetchers, libraryOptions.SubtitleFetcherOrder, true, diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 1134baf92d..7188e9804e 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -8,14 +8,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Providers; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -57,16 +55,9 @@ namespace MediaBrowser.Providers.MediaInfo public bool IsLogged => true; - private SubtitleOptions GetOptions() - { - return _config.GetConfiguration("subtitles"); - } - /// public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) { - var options = GetOptions(); - var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie }; var dict = new Dictionary(); @@ -81,17 +72,14 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.SubtitleDownloadLanguages is null) { - subtitleDownloadLanguages = options.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + // Skip this library if subtitle download languages are not configured + continue; } + subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + foreach (var lang in subtitleDownloadLanguages) { var query = new InternalItemsQuery @@ -144,7 +132,7 @@ namespace MediaBrowser.Providers.MediaInfo try { - await DownloadSubtitles(video as Video, options, cancellationToken).ConfigureAwait(false); + await DownloadSubtitles(video as Video, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -160,7 +148,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - private async Task DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken) + private async Task DownloadSubtitles(Video video, CancellationToken cancellationToken) { var mediaStreams = video.GetMediaStreams(); @@ -173,19 +161,15 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.SubtitleDownloadLanguages is null) { - subtitleDownloadLanguages = options.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - requirePerfectMatch = options.RequirePerfectMatch; - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + // Subtitle downloading is not configured for this library + return true; } + subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + var downloadedLanguages = await new SubtitleDownloader( _logger, _subtitleManager).DownloadSubtitles( From 4f1ad3fee0abff7244b738ecff792dcf17feab49 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 18 Apr 2026 17:31:29 +0200 Subject: [PATCH 44/44] Update to Jellyfin.XmlTv 10.12.0-pre1 --- Directory.Packages.props | 2 +- MediaBrowser.Controller/LiveTv/ProgramInfo.cs | 44 +++++++++---------- .../Listings/XmlTvListingsProvider.cs | 41 ++++++++--------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 34e6a02ff9..d2b715d6aa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 3c3ac2471f..905aad17b9 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -12,45 +10,45 @@ namespace MediaBrowser.Controller.LiveTv { public ProgramInfo() { - Genres = new List(); + Genres = []; - ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); - SeriesProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + ProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); + SeriesProviderIds = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// Gets or sets the id of the program. /// - public string Id { get; set; } + public string? Id { get; set; } /// /// Gets or sets the channel identifier. /// /// The channel identifier. - public string ChannelId { get; set; } + public string? ChannelId { get; set; } /// /// Gets or sets the name of the program. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets or sets the official rating. /// /// The official rating. - public string OfficialRating { get; set; } + public string? OfficialRating { get; set; } /// /// Gets or sets the overview. /// /// The overview. - public string Overview { get; set; } + public string? Overview { get; set; } /// /// Gets or sets the short overview. /// /// The short overview. - public string ShortOverview { get; set; } + public string? ShortOverview { get; set; } /// /// Gets or sets the start date of the program, in UTC. @@ -108,25 +106,25 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the episode title. /// /// The episode title. - public string EpisodeTitle { get; set; } + public string? EpisodeTitle { get; set; } /// /// Gets or sets the image path if it can be accessed directly from the file system. /// /// The image path. - public string ImagePath { get; set; } + public string? ImagePath { get; set; } /// /// Gets or sets the image url if it can be downloaded. /// /// The image URL. - public string ImageUrl { get; set; } + public string? ImageUrl { get; set; } - public string ThumbImageUrl { get; set; } + public string? ThumbImageUrl { get; set; } - public string LogoImageUrl { get; set; } + public string? LogoImageUrl { get; set; } - public string BackdropImageUrl { get; set; } + public string? BackdropImageUrl { get; set; } /// /// Gets or sets a value indicating whether this instance has image. @@ -188,19 +186,19 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the home page URL. /// /// The home page URL. - public string HomePageUrl { get; set; } + public string? HomePageUrl { get; set; } /// /// Gets or sets the series identifier. /// /// The series identifier. - public string SeriesId { get; set; } + public string? SeriesId { get; set; } /// /// Gets or sets the show identifier. /// /// The show identifier. - public string ShowId { get; set; } + public string? ShowId { get; set; } /// /// Gets or sets the season number. @@ -218,10 +216,10 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the etag. /// /// The etag. - public string Etag { get; set; } + public string? Etag { get; set; } - public Dictionary ProviderIds { get; set; } + public Dictionary ProviderIds { get; set; } - public Dictionary SeriesProviderIds { get; set; } + public Dictionary SeriesProviderIds { get; set; } } } diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index b5c884bd10..318c3a2d36 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -62,21 +60,21 @@ namespace Jellyfin.LiveTv.Listings _logger.LogInformation("xmltv path: {Path}", info.Path); string cacheFilename = info.Id + ".xml"; - string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); + string cacheDir = Path.Join(_config.ApplicationPaths.CachePath, "xmltv"); + string cacheFile = Path.Join(cacheDir, cacheFilename); - if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) - { - return cacheFile; - } - - // Must check if file exists as parent directory may not exist. if (File.Exists(cacheFile)) { + if (File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) + { + return cacheFile; + } + File.Delete(cacheFile); } else { - Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + Directory.CreateDirectory(cacheDir); } if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) @@ -154,22 +152,25 @@ namespace Jellyfin.LiveTv.Listings private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) { - string episodeTitle = program.Episode.Title; + string? episodeTitle = program.Episode?.Title; var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); + var imageUrl = program.Icons.FirstOrDefault()?.Source; + var rating = program.Ratings.FirstOrDefault()?.Value; + var starRating = program.StarRatings?.FirstOrDefault()?.StarRating; var programInfo = new ProgramInfo { ChannelId = program.ChannelId, EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode.Episode, + EpisodeNumber = program.Episode?.Episode, EpisodeTitle = episodeTitle, Genres = programCategories, StartDate = program.StartDate.UtcDateTime, Name = program.Title, Overview = program.Description, ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode.Series, - IsSeries = program.Episode.Episode is not null, + SeasonNumber = program.Episode?.Series, + IsSeries = program.Episode?.Episode is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, IsLive = program.IsLive, @@ -177,11 +178,11 @@ namespace Jellyfin.LiveTv.Listings IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, - HasImage = !string.IsNullOrEmpty(program.Icon?.Source), - OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, - CommunityRating = program.StarRating, - SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ImageUrl = string.IsNullOrEmpty(imageUrl) ? null : imageUrl, + HasImage = !string.IsNullOrEmpty(imageUrl), + OfficialRating = string.IsNullOrEmpty(rating) ? null : rating, + CommunityRating = starRating is null ? null : (float)starRating.Value, + SeriesId = program.Episode?.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -262,7 +263,7 @@ namespace Jellyfin.LiveTv.Listings { Id = c.Id, Name = c.DisplayName, - ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, + ImageUrl = string.IsNullOrEmpty(c.Icons.FirstOrDefault()?.Source) ? null : c.Icons.FirstOrDefault()!.Source, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number }).ToList(); }