From c2e5081d64e519a74d47df23335bb228fea8ec7e Mon Sep 17 00:00:00 2001 From: evanreichard <30810613+evanreichard@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:17:43 -0500 Subject: [PATCH 001/131] feat(sqlite): add timeout config (#15369) --- .../Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 2b000b257b..da63df8e29 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -64,6 +64,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse, () => SqliteCacheMode.Default); sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true); + sqliteConnectionBuilder.DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30); var connectionString = sqliteConnectionBuilder.ToString(); From 63a3e552978010e905ea9b4e258c0c2e153ddfec Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 7 Nov 2025 20:18:24 -0500 Subject: [PATCH 002/131] Fix search terms using diacritics (#15435) --- .../Item/BaseItemRepository.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b939c4ab21..94789fe6fa 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1701,15 +1701,16 @@ public sealed class BaseItemRepository if (!string.IsNullOrEmpty(filter.SearchTerm)) { - var searchTerm = filter.SearchTerm.ToLower(); - if (SearchWildcardTerms.Any(f => searchTerm.Contains(f))) + var cleanedSearchTerm = GetCleanValue(filter.SearchTerm); + var originalSearchTerm = filter.SearchTerm.ToLower(); + if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f))) { - searchTerm = $"%{searchTerm.Trim('%')}%"; - baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!.ToLower(), searchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), searchTerm))); + cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%"; + baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm))); } else { - baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm))); + baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm))); } } From d1406302085148aea4d3b03dab78c4d49909c4fe Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 8 Nov 2025 02:19:30 +0100 Subject: [PATCH 003/131] Update branding in Swagger page (#15422) --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- .../wwwroot/api-docs/banner-dark.svg | 34 ------------------- Jellyfin.Server/wwwroot/api-docs/jellyfin.svg | 26 ++++++++++++++ .../wwwroot/api-docs/swagger/custom.css | 12 ++++--- 4 files changed, 34 insertions(+), 40 deletions(-) delete mode 100644 Jellyfin.Server/wwwroot/api-docs/banner-dark.svg create mode 100644 Jellyfin.Server/wwwroot/api-docs/jellyfin.svg diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index df630922a0..14ab114fb4 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -78,7 +78,7 @@ PreserveNewest - + PreserveNewest diff --git a/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg b/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg deleted file mode 100644 index b62b7545c7..0000000000 --- a/Jellyfin.Server/wwwroot/api-docs/banner-dark.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - \ No newline at end of file diff --git a/Jellyfin.Server/wwwroot/api-docs/jellyfin.svg b/Jellyfin.Server/wwwroot/api-docs/jellyfin.svg new file mode 100644 index 0000000000..692530319b --- /dev/null +++ b/Jellyfin.Server/wwwroot/api-docs/jellyfin.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css index acb59888e0..c14ad60215 100644 --- a/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css +++ b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css @@ -4,12 +4,14 @@ } .topbar-wrapper .link:after { - content: url(../banner-dark.svg); + content: ''; display: block; - -moz-box-sizing: border-box; + background-image: url(../jellyfin.svg); + background-position: center; + background-repeat: no-repeat; + background-size: contain; box-sizing: border-box; - max-width: 100%; - max-height: 100%; - width: 150px; + width: 220px; + height: 40px; } /* end logo */ From 8f71922734d42591b3236f4c52d9692f1b191da2 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 7 Nov 2025 20:20:10 -0500 Subject: [PATCH 004/131] Fix item count display for collapsed items (#15380) --- MediaBrowser.Controller/Entities/Folder.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 03ee447088..9b382a8c08 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -729,9 +729,7 @@ namespace MediaBrowser.Controller.Entities query.StartIndex = startIndex; } - var result = PostFilterAndSort(items, query); - result.TotalRecordCount = totalCount; - return result; + return PostFilterAndSort(items, query); } if (this is not UserRootFolder @@ -1001,9 +999,7 @@ namespace MediaBrowser.Controller.Entities items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); } - var result = PostFilterAndSort(items, query); - result.TotalRecordCount = totalItemCount; - return result; + return PostFilterAndSort(items, query); } protected QueryResult PostFilterAndSort(IEnumerable items, InternalItemsQuery query) @@ -1039,7 +1035,15 @@ namespace MediaBrowser.Controller.Entities items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } - return UserViewBuilder.SortAndPage(items, null, query, LibraryManager); + var filteredItems = items as IReadOnlyList ?? items.ToList(); + var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager); + + if (query.EnableTotalRecordCount) + { + result.TotalRecordCount = filteredItems.Count; + } + + return result; } private static IEnumerable CollapseBoxSetItemsIfNeeded( From 91c3b1617e06283c88f36bc63046b99c993cb774 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 8 Nov 2025 03:20:42 +0200 Subject: [PATCH 005/131] Fixed missing sort argument (#15413) --- .../Item/BaseItemRepository.cs | 17 +++-- .../Item/OrderMapper.cs | 72 +++++++++++-------- .../Item/OrderMapperTests.cs | 2 +- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 94789fe6fa..9524f71a56 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -275,6 +275,7 @@ public sealed class BaseItemRepository } dbQuery = ApplyQueryPaging(dbQuery, filter); + dbQuery = ApplyNavigations(dbQuery, filter); result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); result.StartIndex = filter.StartIndex ?? 0; @@ -294,6 +295,7 @@ public sealed class BaseItemRepository dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); + dbQuery = ApplyNavigations(dbQuery, filter); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); } @@ -337,6 +339,8 @@ public sealed class BaseItemRepository mainquery = ApplyGroupingFilter(context, mainquery, filter); mainquery = ApplyQueryPaging(mainquery, filter); + mainquery = ApplyNavigations(mainquery, filter); + return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); } @@ -399,9 +403,7 @@ public sealed class BaseItemRepository dbQuery = dbQuery.Distinct(); } - dbQuery = ApplyOrder(dbQuery, filter); - - dbQuery = ApplyNavigations(dbQuery, filter); + dbQuery = ApplyOrder(dbQuery, filter, context); return dbQuery; } @@ -446,6 +448,7 @@ public sealed class BaseItemRepository dbQuery = TranslateQuery(dbQuery, context, filter); dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); + dbQuery = ApplyNavigations(dbQuery, filter); return dbQuery; } @@ -1252,7 +1255,7 @@ public sealed class BaseItemRepository .AsSingleQuery() .Where(e => masterQuery.Contains(e.Id)); - query = ApplyOrder(query, filter); + query = ApplyOrder(query, filter, context); var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); if (filter.EnableTotalRecordCount) @@ -1518,7 +1521,7 @@ public sealed class BaseItemRepository || query.IncludeItemTypes.Contains(BaseItemKind.Season); } - private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter) + private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter, JellyfinDbContext context) { var orderBy = filter.OrderBy; var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); @@ -1537,7 +1540,7 @@ public sealed class BaseItemRepository var firstOrdering = orderBy.FirstOrDefault(); if (firstOrdering != default) { - var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter); + var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context); if (firstOrdering.SortOrder == SortOrder.Ascending) { orderedQuery = query.OrderBy(expression); @@ -1562,7 +1565,7 @@ public sealed class BaseItemRepository foreach (var item in orderBy.Skip(1)) { - var expression = OrderMapper.MapOrderByField(item.OrderBy, filter); + var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context); if (item.SortOrder == SortOrder.Ascending) { orderedQuery = orderedQuery!.ThenBy(expression); diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index a0c1270311..192ee74996 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -1,7 +1,10 @@ +#pragma warning disable RS0030 // Do not use banned APIs + using System; using System.Linq; using System.Linq.Expressions; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using Microsoft.EntityFrameworkCore; @@ -18,39 +21,50 @@ public static class OrderMapper /// /// Item property to sort by. /// Context Query. + /// Context. /// Func to be executed later for sorting query. - public static Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + public static Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query, JellyfinDbContext jellyfinDbContext) { - return sortBy switch + return (sortBy, query.User) switch { - ItemSortBy.AirTime => e => e.SortName, // TODO - ItemSortBy.Runtime => e => e.RunTimeTicks, - ItemSortBy.Random => e => EF.Functions.Random(), - ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, - ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, - ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, - ItemSortBy.IsFolder => e => e.IsFolder, - ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, - ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, - ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, - ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => e => e.SeriesName, + (ItemSortBy.AirTime, _) => e => e.SortName, // TODO + (ItemSortBy.Runtime, _) => e => e.RunTimeTicks, + (ItemSortBy.Random, _) => e => EF.Functions.Random(), + (ItemSortBy.DatePlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, + (ItemSortBy.PlayCount, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, + (ItemSortBy.IsFavoriteOrLiked, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, + (ItemSortBy.IsFolder, _) => e => e.IsFolder, + (ItemSortBy.IsPlayed, _) => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, + (ItemSortBy.IsUnplayed, _) => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, + (ItemSortBy.DateLastContentAdded, _) => e => e.DateLastMediaAdded, + (ItemSortBy.Artist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + (ItemSortBy.AlbumArtist, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + (ItemSortBy.Studio, _) => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + (ItemSortBy.OfficialRating, _) => e => e.InheritedParentalRatingValue, + (ItemSortBy.SeriesSortName, _) => e => e.SeriesName, + (ItemSortBy.Album, _) => e => e.Album, + (ItemSortBy.DateCreated, _) => e => e.DateCreated, + (ItemSortBy.PremiereDate, _) => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), + (ItemSortBy.StartDate, _) => e => e.StartDate, + (ItemSortBy.Name, _) => e => e.CleanName, + (ItemSortBy.CommunityRating, _) => e => e.CommunityRating, + (ItemSortBy.ProductionYear, _) => e => e.ProductionYear, + (ItemSortBy.CriticRating, _) => e => e.CriticRating, + (ItemSortBy.VideoBitRate, _) => e => e.TotalBitrate, + (ItemSortBy.ParentIndexNumber, _) => e => e.ParentIndexNumber, + (ItemSortBy.IndexNumber, _) => e => e.IndexNumber, + (ItemSortBy.SeriesDatePlayed, not null) => e => + jellyfinDbContext.BaseItems + .Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey) + .Join(jellyfinDbContext.UserData.Where(w => w.UserId == query.User.Id && w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate) + .Max(f => f), + (ItemSortBy.SeriesDatePlayed, null) => e => jellyfinDbContext.BaseItems.Where(w => w.SeriesPresentationUniqueKey == e.PresentationUniqueKey) + .Join(jellyfinDbContext.UserData.Where(w => w.Played), f => f.Id, f => f.ItemId, (item, userData) => userData.LastPlayedDate) + .Max(f => f), + // ItemSortBy.SeriesDatePlayed => e => jellyfinDbContext.UserData + // .Where(u => u.Item!.SeriesPresentationUniqueKey == e.PresentationUniqueKey && u.Played) + // .Max(f => f.LastPlayedDate), // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => e => e.Album, - ItemSortBy.DateCreated => e => e.DateCreated, - ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), - ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.CleanName, - ItemSortBy.CommunityRating => e => e.CommunityRating, - ItemSortBy.ProductionYear => e => e.ProductionYear, - ItemSortBy.CriticRating => e => e.CriticRating, - ItemSortBy.VideoBitRate => e => e.TotalBitrate, - ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, - ItemSortBy.IndexNumber => e => e.IndexNumber, _ => e => e.SortName }; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs index caf2b06b73..8ac3e5e317 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs @@ -12,7 +12,7 @@ public class OrderMapperTests [Fact] public void ShouldReturnMappedOrderForSortingByPremierDate() { - var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery()).Compile(); + var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery(), null!).Compile(); var expectedDate = new DateTime(1, 2, 3); var expectedProductionYearDate = new DateTime(4, 1, 1); From 097cb87f6f6df662361a4cd536b56470e4cd68a3 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 8 Nov 2025 02:21:10 +0100 Subject: [PATCH 006/131] Don't enforce a minimum amount of free space for the tmp and log dirs (#15390) --- .../StorageHelpers/StorageHelper.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index 570d6cb9b7..ce628a04d0 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -13,7 +13,6 @@ namespace Jellyfin.Server.Implementations.StorageHelpers; public static class StorageHelper { private const long TwoGigabyte = 2_147_483_647L; - private const long FiveHundredAndTwelveMegaByte = 536_870_911L; private static readonly string[] _byteHumanizedSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; /// @@ -24,10 +23,8 @@ public static class StorageHelper public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger) { TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte); - TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte); TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte); TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte); - TestDataDirectorySize(applicationPaths.TempDirectory, logger, FiveHundredAndTwelveMegaByte); } /// From 7222910b05dff772fad22d4f557fad20578fa275 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 7 Nov 2025 20:21:41 -0500 Subject: [PATCH 007/131] Fix filters to use SortName (#15381) --- .../Item/BaseItemRepository.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 9524f71a56..7117e0a8d7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1948,19 +1948,20 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) { - baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith)); + var startsWithLower = filter.NameStartsWith.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower)); } if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]); + var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0); } if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) { - // i hate this - baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]); + var lessThanLower = filter.NameLessThan.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0); } if (filter.ImageTypes.Length > 0) From 002c83e6f5da0751b2b4b3504bb9cd2499782eea Mon Sep 17 00:00:00 2001 From: Carsten Braun Date: Sat, 8 Nov 2025 14:32:14 +0100 Subject: [PATCH 008/131] Fix NullReferenceExceltop when role is null. --- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index bdb6b93beb..7220e3fb1f 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -520,7 +520,7 @@ namespace MediaBrowser.Providers.MediaInfo { Name = person.Name, Type = person.Type, - Role = person.Role.Trim() + Role = person.Role = person.Role?.Trim() }); } } From 90a8a26c6e3009335ff76b0a784be5644dfe3a03 Mon Sep 17 00:00:00 2001 From: Carsten Braun Date: Sat, 8 Nov 2025 15:00:11 +0100 Subject: [PATCH 009/131] Copy-Pasting is sometimes hard.... --- MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 7220e3fb1f..bde23e842f 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -520,7 +520,7 @@ namespace MediaBrowser.Providers.MediaInfo { Name = person.Name, Type = person.Type, - Role = person.Role = person.Role?.Trim() + Role = person.Role?.Trim() }); } } From 49efd68fc7ef4b70b38151a177502bbdb3adede0 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 8 Nov 2025 10:30:04 -0500 Subject: [PATCH 010/131] Invalidate parent folder's cache on deletion/creation (#15423) --- .../Library/LibraryManager.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a400cb0925..cab87e53de 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -457,6 +457,12 @@ namespace Emby.Server.Implementations.Library _cache.TryRemove(child.Id, out _); } + if (parent is Folder folder) + { + folder.Children = null; + folder.UserData = null; + } + ReportItemRemoved(item, parent); } @@ -1993,6 +1999,12 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } + if (parent is Folder folder) + { + folder.Children = null; + folder.UserData = null; + } + if (ItemAdded is not null) { foreach (var item in items) @@ -2150,6 +2162,12 @@ namespace Emby.Server.Implementations.Library _itemRepository.SaveItems(items, cancellationToken); + if (parent is Folder folder) + { + folder.Children = null; + folder.UserData = null; + } + if (ItemUpdated is not null) { foreach (var item in items) From 177b6464ca1b6772a191dbf9c5595708f91fc0fa Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 9 Nov 2025 11:22:09 -0500 Subject: [PATCH 011/131] Don't clear baseitemids (#15446) --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index b90da9f7d3..d221d18531 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -383,8 +383,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine }); } - baseItemIds.Clear(); - foreach (var item in peopleCache) { operation.JellyfinDbContext.Peoples.Add(item.Value.Person); From 13c4517a66e3f857cc4acc9b2fa3505297d554eb Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 9 Nov 2025 11:35:50 -0500 Subject: [PATCH 012/131] Fix collection grouping in mixed libraries (#15373) --- MediaBrowser.Controller/Entities/Folder.cs | 75 ++++++++++++++++------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 9b382a8c08..151b957fe9 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1056,12 +1056,49 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(items); - if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) + if (!CollapseBoxSetItems(query, queryParent, user, configurationManager)) { - items = collectionManager.CollapseItemsWithinBoxSets(items, user); + return items; } - return items; + var config = configurationManager.Configuration; + + bool collapseMovies = config.EnableGroupingMoviesIntoCollections; + bool collapseSeries = config.EnableGroupingShowsIntoCollections; + + if (user is null || (collapseMovies && collapseSeries)) + { + return collectionManager.CollapseItemsWithinBoxSets(items, user); + } + + if (!collapseMovies && !collapseSeries) + { + return items; + } + + var collapsibleItems = new List(); + var remainingItems = new List(); + + foreach (var item in items) + { + if ((collapseMovies && item is Movie) || (collapseSeries && item is Series)) + { + collapsibleItems.Add(item); + } + else + { + remainingItems.Add(item); + } + } + + if (collapsibleItems.Count == 0) + { + return remainingItems; + } + + var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user); + + return collapsedItems.Concat(remainingItems); } private static bool CollapseBoxSetItems( @@ -1092,24 +1129,26 @@ namespace MediaBrowser.Controller.Entities } var param = query.CollapseBoxSetItems; - - if (!param.HasValue) + if (param.HasValue) { - if (user is not null && query.IncludeItemTypes.Any(type => - (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) || - (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections))) - { - return false; - } - - if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series)) - { - param = true; - } + return param.Value && AllowBoxSetCollapsing(query); } - return param.HasValue && param.Value && AllowBoxSetCollapsing(query); + var config = configurationManager.Configuration; + + bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie); + bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series); + + bool collapseMovies = config.EnableGroupingMoviesIntoCollections; + bool collapseSeries = config.EnableGroupingShowsIntoCollections; + + if (user is not null) + { + bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries); + return canCollapse && AllowBoxSetCollapsing(query); + } + + return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query); } private static bool AllowBoxSetCollapsing(InternalItemsQuery request) From 3b2d64995aab63ebaa6832c059a3cc0bdebe90dc Mon Sep 17 00:00:00 2001 From: "Mikal S." <7761729+revam@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:45:02 +0100 Subject: [PATCH 013/131] Resolve symlinks for static media source infos (#15263) --- .../IO/ManagedFileSystem.cs | 3 +- .../Library/DotIgnoreIgnoreRule.cs | 3 +- .../Trickplay/TrickplayManager.cs | 4 +- MediaBrowser.Controller/Entities/BaseItem.cs | 12 ++- .../IO/FileSystemHelper.cs | 74 +++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 97e89ca3d9..fad97344b5 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -260,7 +261,7 @@ namespace Emby.Server.Implementations.IO { try { - var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true); + var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true); if (targetFileInfo is not null) { result.Exists = targetFileInfo.Exists; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 959acd4751..e53502046a 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,7 @@ using System; using System.IO; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.IO; @@ -92,7 +93,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule private static string GetFileContent(FileInfo dirIgnoreFile) { - dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile; + dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; if (!dirIgnoreFile.Exists) { return string.Empty; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 6f2d2a1071..4505a377ce 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager } // We support video backdrops, but we should not generate trickplay images for them - var parentDirectory = Directory.GetParent(mediaPath); + var parentDirectory = Directory.GetParent(video.Path); if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id); + _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id); return; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4989f0f3f6..3c46d53e5c 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,6 +24,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; @@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities var protocol = item.PathProtocol; + // Resolve the item path so everywhere we use the media source it will always point to + // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link + // path will return null, so it's safe to check for all paths. + var itemPath = item.Path; + if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo) + { + itemPath = linkInfo.FullName; + } + var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), @@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), - Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 1a33c3aa8c..324aea7e3b 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Model.IO; @@ -61,4 +62,77 @@ public static class FileSystemHelper } } } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The path of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + // Check if the file exists so the native resolve handler won't throw at us. + if (!File.Exists(linkPath)) + { + return null; + } + + if (!returnFinalTarget) + { + return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + } + + if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) + { + return null; + } + + var currentPath = targetInfo.FullName; + var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; + while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + { + var targetPath = linkInfo.FullName; + + // If an infinite loop is detected, return the file info for the + // first link in the loop we encountered. + if (!visited.Add(targetPath)) + { + return new FileInfo(targetPath); + } + + targetInfo = linkInfo; + currentPath = targetPath; + + // Exit if the target doesn't exist, so the native resolve handler won't throw at us. + if (!targetInfo.Exists) + { + break; + } + } + + return targetInfo; + } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The file info of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false) + { + ArgumentNullException.ThrowIfNull(fileInfo); + + return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget); + } } From 511223aac4da42d32825e9ef6c55e624213c07fc Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 10 Nov 2025 02:30:49 -0500 Subject: [PATCH 014/131] Fix NullReferenceException in GetPathProtocol when path is null --- Emby.Server.Implementations/Library/MediaSourceManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 750346169f..c667fb0600 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.Library /// > public MediaProtocol GetPathProtocol(string path) { + if (string.IsNullOrEmpty(path)) + { + return MediaProtocol.File; + } + if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) { return MediaProtocol.Rtsp; From 3c3c2aee0db0ce8f9068f8e8ff661a560973192b Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 10 Nov 2025 23:19:17 +0100 Subject: [PATCH 015/131] Check if target exists before trying to follow it Exception got caught in ManagedFileSystem and wrong file info got returned --- MediaBrowser.Controller/IO/FileSystemHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 324aea7e3b..3e390ca428 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -92,6 +92,11 @@ public static class FileSystemHelper return null; } + if (!targetInfo.Exists) + { + return targetInfo; + } + var currentPath = targetInfo.FullName; var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) From f4a846aa4dcffb3be7b701f806b24cb8dd6b7c5d Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 11 Nov 2025 23:45:47 +0100 Subject: [PATCH 016/131] Don't error out when searching for marker files fails (#15466) Fixes #15445 --- .../AppBase/BaseApplicationPaths.cs | 14 ++++++++++++-- .../Configuration/IApplicationPaths.cs | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index c69bcfef78..de722332a4 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -107,10 +107,20 @@ namespace Emby.Server.Implementations.AppBase private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) { - var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); + string? otherMarkers = null; + try + { + otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase)); + } + catch + { + // Error while checking for marker files, assume none exist and keep going + // TODO: add some logging + } + if (otherMarkers is not null) { - throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); + throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}."); } var markerPath = Path.Combine(path, markerName); diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 6d1a72b042..3a61974901 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -103,11 +103,11 @@ namespace MediaBrowser.Common.Configuration void MakeSanityCheckOrThrow(); /// - /// Checks and creates the given path and adds it with a marker file if non existant. + /// Checks and creates the given path and adds it with a marker file if non existent. /// /// The path to check. /// The common marker file name. - /// Check for other settings paths recursivly. + /// Check for other settings paths recursively. void CreateAndCheckMarker(string path, string markerName, bool recursive = false); } } From 2e5ced50986c37b19b5f4ef34d730fc56a51535a Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 12 Nov 2025 19:36:57 -0500 Subject: [PATCH 017/131] Improve season folder parsing (#15404) --- Emby.Naming/TV/SeasonPathParser.cs | 55 +++++++++---------- .../TV/SeasonPathParserTests.cs | 6 ++ 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index eafb09a6a3..72adfb2d96 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -10,12 +10,17 @@ namespace Emby.Naming.TV /// public static partial class SeasonPathParser { + private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled); + [GeneratedRegex(@"^\s*((?(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPre(); - [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?(?>\d+)(?!\s*[Ee]\d+))(?.*)$", RegexOptions.IgnoreCase)] + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?.*)$", RegexOptions.IgnoreCase)] private static partial Regex ProcessPost(); + [GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)] + private static partial Regex SeasonPrefix(); + /// /// Attempts to parse season number from path. /// @@ -56,44 +61,34 @@ namespace Emby.Naming.TV bool supportSpecialAliases, bool supportNumericSeasonFolders) { - string filename = Path.GetFileName(path); - filename = Regex.Replace(filename, "[ ._-]", string.Empty); + var fileName = Path.GetFileName(path); + + var seasonPrefixMatch = SeasonPrefix().Match(fileName); + if (seasonPrefixMatch.Success && + int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + return (val, true); + } + + string filename = CleanNameRegex.Replace(fileName, string.Empty); if (parentFolderName is not null) { - parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); - filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); + var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty); + filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase); } - if (supportSpecialAliases) + if (supportSpecialAliases && + (filename.Equals("specials", StringComparison.OrdinalIgnoreCase) || + filename.Equals("extras", StringComparison.OrdinalIgnoreCase))) { - if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) - { - return (0, true); - } - - if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase)) - { - return (0, true); - } + return (0, true); } - if (supportNumericSeasonFolders) + if (supportNumericSeasonFolders && + int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val)) { - if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } - } - - if (filename.Length > 0 && (filename[0] == 'S' || filename[0] == 's')) - { - var testFilename = filename.AsSpan()[1..]; - - if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return (val, true); - } + return (val, true); } var preMatch = ProcessPre().Match(filename); diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 0c3671f4fb..4dbe769bf4 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -69,6 +69,12 @@ public class SeasonPathParserTests [InlineData("/media/YouTube/Devyn Johnston/2024-01-24 4070 Ti SUPER in under 7 minutes", "/media/YouTube/Devyn Johnston", null, false)] [InlineData("/media/YouTube/Devyn Johnston/2025-01-28 5090 vs 2 SFF Cases", "/media/YouTube/Devyn Johnston", null, false)] [InlineData("/Drive/202401244070", "/Drive", null, false)] + [InlineData("/Drive/Drive.S01.2160p.WEB-DL.DDP5.1.H.265-XXXX", "/Drive", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.1080p.PDTV.x264-JCH", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/[The.Wonder.Years.S04.1080p.PDTV.x264-JCH]", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/The.Wonder.Years [S04][1080p.PDTV.x264-JCH]", "/The Wonder Years", 4, true)] + [InlineData("The Wonder Years/The Wonder Years Season 01 1080p", "/The Wonder Years", 1, true)] + public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { var result = SeasonPathParser.Parse(path, parentPath, true, true); From 435bb14bb266916e9c6f100c4324a94c36126e06 Mon Sep 17 00:00:00 2001 From: Huo Jiacheng Date: Thu, 13 Nov 2025 10:43:13 +0800 Subject: [PATCH 018/131] Fix gitignore-style not working properly on windows. (#15487) --- .../Library/DotIgnoreIgnoreRule.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index e53502046a..46e60dbaa4 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -88,6 +89,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule var ignore = new Ignore.Ignore(); ignore.Add(ignoreRules); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Mitigate the problem of the Ignore library not handling Windows paths correctly. + // See https://github.com/jellyfin/jellyfin/issues/15484 + return ignore.IsIgnored(fileInfo.FullName.NormalizePath('/')); + } + return ignore.IsIgnored(fileInfo.FullName); } From 4b38e35bbbb65c77f251288ff64ee28da4a48943 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 13 Nov 2025 20:23:03 -0500 Subject: [PATCH 019/131] Remove InheritedTags and update tag filtering logic (#15493) --- .../Item/BaseItemRepository.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 7117e0a8d7..2c18ce69ac 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2420,39 +2420,34 @@ public sealed class BaseItemRepository if (filter.ExcludeInheritedTags.Length > 0) { - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))); + baseQuery = baseQuery.Where(e => + !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)) + && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue || + !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)))); } if (filter.IncludeInheritedTags.Length > 0) { - // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client. - // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well. - if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode) + // For seasons and episodes, we also need to check the parent series' tags. + if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season)) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || - (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)))); + baseQuery = baseQuery.Where(e => + e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) + || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)))); } // A playlist should be accessible to its owner regardless of allowed tags. else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); + baseQuery = baseQuery.Where(e => + e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) + || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); // d ^^ this is stupid it hate this. } else { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags) - .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); + baseQuery = baseQuery.Where(e => + e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); } } From e8150428b62668e062a3432960f98684d3b352cb Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 13 Nov 2025 20:23:18 -0500 Subject: [PATCH 020/131] Fix .ignore handling for directories (#15501) --- .../Library/DotIgnoreIgnoreRule.cs | 103 +++++++----------- 1 file changed, 42 insertions(+), 61 deletions(-) diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 46e60dbaa4..473ff8e1d7 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Runtime.InteropServices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -13,28 +12,24 @@ namespace Emby.Server.Implementations.Library; /// public class DotIgnoreIgnoreRule : IResolverIgnoreRule { + private static readonly bool IsWindows = OperatingSystem.IsWindows(); + private static FileInfo? FindIgnoreFile(DirectoryInfo directory) { - var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); - if (ignoreFile.Exists) + for (var current = directory; current is not null; current = current.Parent) { - return ignoreFile; + var ignorePath = Path.Join(current.FullName, ".ignore"); + if (File.Exists(ignorePath)) + { + return new FileInfo(ignorePath); + } } - var parentDir = directory.Parent; - if (parentDir is null) - { - return null; - } - - return FindIgnoreFile(parentDir); + return null; } /// - public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) - { - return IsIgnored(fileInfo, parent); - } + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent); /// /// Checks whether or not the file is ignored. @@ -44,72 +39,58 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule /// True if the file should be ignored. public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) { - if (fileInfo.IsDirectory) - { - var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); - if (dirIgnoreFile is null) - { - return false; - } + var searchDirectory = fileInfo.IsDirectory + ? new DirectoryInfo(fileInfo.FullName) + : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty); - // Fast path in case the ignore files isn't a symlink and is empty - if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0) - { - return true; - } - - // ignore the directory only if the .ignore file is empty - // evaluate individual files otherwise - return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); - } - - var parentDirPath = Path.GetDirectoryName(fileInfo.FullName); - if (string.IsNullOrEmpty(parentDirPath)) + if (string.IsNullOrEmpty(searchDirectory.FullName)) { return false; } - var folder = new DirectoryInfo(parentDirPath); - var ignoreFile = FindIgnoreFile(folder); + var ignoreFile = FindIgnoreFile(searchDirectory); if (ignoreFile is null) { return false; } - string ignoreFileString = GetFileContent(ignoreFile); - - if (string.IsNullOrWhiteSpace(ignoreFileString)) + // Fast path in case the ignore files isn't a symlink and is empty + if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0) { // Ignore directory if we just have the file return true; } - // If file has content, base ignoring off the content .gitignore-style rules - var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); - var ignore = new Ignore.Ignore(); - ignore.Add(ignoreRules); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Mitigate the problem of the Ignore library not handling Windows paths correctly. - // See https://github.com/jellyfin/jellyfin/issues/15484 - return ignore.IsIgnored(fileInfo.FullName.NormalizePath('/')); - } - - return ignore.IsIgnored(fileInfo.FullName); + var content = GetFileContent(ignoreFile); + return string.IsNullOrWhiteSpace(content) + || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory); } - private static string GetFileContent(FileInfo dirIgnoreFile) + private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory) { - dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; - if (!dirIgnoreFile.Exists) + // If file has content, base ignoring off the content .gitignore-style rules + var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var ignore = new Ignore.Ignore(); + ignore.Add(rules); + + // Mitigate the problem of the Ignore library not handling Windows paths correctly. + // See https://github.com/jellyfin/jellyfin/issues/15484 + var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + + // Add trailing slash for directories to match "folder/" + if (isDirectory) { - return string.Empty; + pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/"); } - using (var reader = dirIgnoreFile.OpenText()) - { - return reader.ReadToEnd(); - } + return ignore.IsIgnored(pathToCheck); + } + + private static string GetFileContent(FileInfo ignoreFile) + { + ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile; + return ignoreFile.Exists + ? File.ReadAllText(ignoreFile.FullName) + : string.Empty; } } From ee34c75386cc1a0ca0e15196a43b685fa0e73130 Mon Sep 17 00:00:00 2001 From: Iksas Date: Fri, 14 Nov 2025 02:30:18 +0100 Subject: [PATCH 021/131] fix missing font extraction for certain transcoding settings (#15502) --- MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 0cda803d64..2fd054f110 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -396,7 +396,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath); // If subtitles get burned in fonts may need to be extracted from the media file - if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (state.SubtitleStream is not null && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode || state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding)) { if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { From 078f9584ed3622eed3516488026cbb6e42242bba Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 14 Nov 2025 17:19:40 -0500 Subject: [PATCH 022/131] Fix playlist DateCreated and DateLastMediaAdded not being set (#15508) --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 1 + MediaBrowser.Providers/Manager/MetadataService.cs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index c9d76df0bf..1577c5c9c7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -244,6 +244,7 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = [.. playlist.LinkedChildren, .. childrenToAdd]; + playlist.DateLastMediaAdded = DateTime.UtcNow; await UpdatePlaylistInternal(playlist).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 4c83845992..d89c545d87 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -344,7 +344,10 @@ namespace MediaBrowser.Providers.Manager item.DateModified = info.LastWriteTimeUtc; if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded) { - item.DateCreated = info.CreationTimeUtc; + if (info.CreationTimeUtc > DateTime.MinValue) + { + item.DateCreated = info.CreationTimeUtc; + } } if (item is Video video) From 6566188e453b42604dbb3ce532937951e88565d0 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 15 Nov 2025 10:39:25 -0500 Subject: [PATCH 023/131] Add 1 minute tolerance for NFO change detection (#15514) --- MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index c671e7a932..5ac672f105 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -68,12 +68,15 @@ namespace MediaBrowser.XbmcMetadata.Providers { var file = GetXmlFile(new ItemInfo(item), directoryService); - if (file is null) + if (file?.Exists is not true) { return false; } - return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; + var fileTime = _fileSystem.GetLastWriteTimeUtc(file); + + // 1 minute tolerance to avoid detecting our own file writes + return (fileTime - item.DateLastSaved) > TimeSpan.FromMinutes(1); } protected abstract void Fetch(MetadataResult result, string path, CancellationToken cancellationToken); From abfbaca33686ab5214182992ab644a6e24d4b180 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 16 Nov 2025 15:35:43 -0500 Subject: [PATCH 024/131] Fix series DateLastMediaAdded not updating when new episodes are added (#15472) --- .../Manager/MetadataService.cs | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index d89c545d87..c8435aa681 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -317,12 +317,8 @@ namespace MediaBrowser.Providers.Manager { if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType)) { - if (isFullRefresh || updateType > ItemUpdateType.None) - { - var children = GetChildrenForMetadataUpdates(item); - - updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType); - } + var children = GetChildrenForMetadataUpdates(item); + updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType); } var presentationUniqueKey = item.CreatePresentationUniqueKey(); @@ -365,16 +361,24 @@ namespace MediaBrowser.Providers.Manager protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType) { - if (isFullRefresh || currentUpdateType > ItemUpdateType.None) + if (item is Folder folder) { - if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren) + if (!isFullRefresh && currentUpdateType == ItemUpdateType.None) { - return true; + return folder.SupportsDateLastMediaAdded; } - if (item is Folder folder) + if (isFullRefresh || currentUpdateType > ItemUpdateType.None) { - return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks; + if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosFromChildren || EnableUpdatingOfficialRatingFromChildren) + { + return true; + } + + if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks) + { + return true; + } } } @@ -395,36 +399,42 @@ namespace MediaBrowser.Providers.Manager { var updateType = ItemUpdateType.None; - if (isFullRefresh || currentUpdateType > ItemUpdateType.None) + if (item is Folder folder) { - updateType |= UpdateCumulativeRunTimeTicks(item, children); - updateType |= UpdateDateLastMediaAdded(item, children); - - // don't update user-changeable metadata for locked items - if (item.IsLocked) + if (folder.SupportsDateLastMediaAdded) { - return updateType; + updateType |= UpdateDateLastMediaAdded(item, children); } - if (EnableUpdatingPremiereDateFromChildren) + if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks) { - updateType |= UpdatePremiereDate(item, children); + updateType |= UpdateCumulativeRunTimeTicks(item, children); } + } - if (EnableUpdatingGenresFromChildren) - { - updateType |= UpdateGenres(item, children); - } + if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked) + { + return updateType; + } - if (EnableUpdatingStudiosFromChildren) - { - updateType |= UpdateStudios(item, children); - } + if (EnableUpdatingPremiereDateFromChildren) + { + updateType |= UpdatePremiereDate(item, children); + } - if (EnableUpdatingOfficialRatingFromChildren) - { - updateType |= UpdateOfficialRating(item, children); - } + if (EnableUpdatingGenresFromChildren) + { + updateType |= UpdateGenres(item, children); + } + + if (EnableUpdatingStudiosFromChildren) + { + updateType |= UpdateStudios(item, children); + } + + if (EnableUpdatingOfficialRatingFromChildren) + { + updateType |= UpdateOfficialRating(item, children); } return updateType; From def5956cd1afe8848c0e232fa477720c4158832f Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 16 Nov 2025 15:36:35 -0500 Subject: [PATCH 025/131] Fix tmdbid not detected in single movie folder (#14955) --- .../Library/Resolvers/Movies/MovieResolver.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 333c8c34bf..98e8f5350b 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -369,13 +369,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies // We need to only look at the name of this actual item (not parents) var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan()); - if (!justName.IsEmpty) + var tmdbid = justName.GetAttributeValue("tmdbid"); + + // If not in a mixed folder and ID not found in folder path, check filename + if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder) { - // Check for TMDb id - var tmdbid = justName.GetAttributeValue("tmdbid"); - item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid); + tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid"); } + item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid); + if (!string.IsNullOrEmpty(item.Path)) { // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name) From f8e012582a8819d18ad933fd65eade936bdc946d Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 16 Nov 2025 15:59:58 -0500 Subject: [PATCH 026/131] Fix movie titles using folder name when NFOs saver is enabled (#15529) --- MediaBrowser.Providers/Manager/MetadataService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index c8435aa681..f220ec4a14 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -151,7 +151,10 @@ namespace MediaBrowser.Providers.Manager .ConfigureAwait(false); updateType |= beforeSaveResult; - updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + if (!isFirstRefresh) + { + updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + } // Next run metadata providers if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) From 921d7d336483c5fdee3dec3803eec1d5d7e6c705 Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Sun, 16 Nov 2025 17:40:07 -0500 Subject: [PATCH 027/131] Bump version to 10.11.3 --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 6337e52326..a5e613cf60 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ Jellyfin Contributors Jellyfin.Naming - 10.11.2 + 10.11.3 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index d55dc596af..ab049706cc 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ Jellyfin Contributors Jellyfin.Data - 10.11.2 + 10.11.3 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 7b056a3c87..ef4fdfdb01 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Common - 10.11.2 + 10.11.3 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 54826a1ceb..737f0c3a3d 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Controller - 10.11.2 + 10.11.3 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index c12105a36f..120338d4ee 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Model - 10.11.2 + 10.11.3 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/SharedVersion.cs b/SharedVersion.cs index 1ebb9b88ae..8e24a20ade 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.2")] -[assembly: AssemblyFileVersion("10.11.2")] +[assembly: AssemblyVersion("10.11.3")] +[assembly: AssemblyFileVersion("10.11.3")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 439a3d9d00..61421b2259 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ Jellyfin Contributors Jellyfin.Extensions - 10.11.2 + 10.11.3 https://github.com/jellyfin/jellyfin GPL-3.0-only From ee7ad83427ed30aa095896ff3577bb946f3d1c02 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 19 Nov 2025 09:36:59 +0800 Subject: [PATCH 028/131] Restrict first video frame probing to file protocol (#15557) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index b7fef842b3..73c5b88c8b 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -511,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Encoder ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; - if (!isAudio && _proberSupportsFirstVideoFrame) + if (protocol == MediaProtocol.File && !isAudio && _proberSupportsFirstVideoFrame) { args += " -show_frames -only_first_vframe"; } From 5ae444d96d473ba42c4a812c3f366b0faa6ebef4 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 18 Nov 2025 20:37:09 -0500 Subject: [PATCH 029/131] Fix NullReferenceException in filesystem path comparison (#15548) --- Emby.Server.Implementations/IO/ManagedFileSystem.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index fad97344b5..4d68cb4444 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -497,8 +497,17 @@ namespace Emby.Server.Implementations.IO /// public virtual bool AreEqual(string path1, string path2) { - return Path.TrimEndingDirectorySeparator(path1).Equals( - Path.TrimEndingDirectorySeparator(path2), + if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2)) + { + return false; + } + + var normalized1 = Path.TrimEndingDirectorySeparator(path1); + var normalized2 = Path.TrimEndingDirectorySeparator(path2); + + return string.Equals( + normalized1, + normalized2, _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } From 1e7e46cb8212385f86564b92d111ad80464f45d0 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 19 Nov 2025 09:37:35 +0800 Subject: [PATCH 030/131] Prevent copying HDR streams when only SDR is supported (#15556) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index a1d8915353..915c787f29 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2378,6 +2378,13 @@ namespace MediaBrowser.Controller.MediaEncoding var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); + // If SDR is the only supported range, we should not copy any of the HDR streams. + // All the following copy check assumes at least one HDR format is supported. + if (requestedRangeTypes.Length == 1 && requestHasSDR && videoStream.VideoRangeType != VideoRangeType.SDR) + { + return false; + } + // If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it. if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI) { From c491a918c21025b105afba4b6d72a24372aac505 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 19 Nov 2025 11:01:13 -0500 Subject: [PATCH 031/131] Save item to database before providers run to prevent FK constraint errors (#15563) --- MediaBrowser.Providers/Manager/MetadataService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f220ec4a14..a2102ca9cd 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -151,9 +151,9 @@ namespace MediaBrowser.Providers.Manager .ConfigureAwait(false); updateType |= beforeSaveResult; - if (!isFirstRefresh) + if (isFirstRefresh) { - updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } // Next run metadata providers From 0ee81e87be58072e21a3bc69fc1d1d0fbc83974a Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 19 Nov 2025 11:02:53 -0500 Subject: [PATCH 032/131] Fix locked fields on not saving (#15564) --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 2c18ce69ac..f4bb94349d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -618,12 +618,18 @@ public sealed class BaseItemRepository { context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete(); + context.BaseItemMetadataFields.Where(e => e.ItemId == entity.Id).ExecuteDelete(); if (entity.Images is { Count: > 0 }) { context.BaseItemImageInfos.AddRange(entity.Images); } + if (entity.LockedFields is { Count: > 0 }) + { + context.BaseItemMetadataFields.AddRange(entity.LockedFields); + } + context.BaseItems.Attach(entity).State = EntityState.Modified; } } From 94f3725208caa030910b62b798ad2f78608d6fd6 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 21 Nov 2025 23:14:03 -0500 Subject: [PATCH 033/131] Fix isMovie filter logic (#15594) --- .../Item/BaseItemRepository.cs | 17 ++++++++--------- MediaBrowser.Controller/Entities/Folder.cs | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f4bb94349d..84168291a8 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1653,19 +1653,18 @@ public sealed class BaseItemRepository var tags = filter.Tags.ToList(); var excludeTags = filter.ExcludeTags.ToList(); - if (filter.IsMovie == true) + if (filter.IsMovie.HasValue) { - if (filter.IncludeItemTypes.Length == 0 - || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) - || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + var shouldIncludeAllMovieTypes = filter.IsMovie.Value + && (filter.IncludeItemTypes.Length == 0 + || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) + || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)); + + if (!shouldIncludeAllMovieTypes) { - baseQuery = baseQuery.Where(e => e.IsMovie); + baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value); } } - else if (filter.IsMovie.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); - } if (filter.IsSeries.HasValue) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 151b957fe9..59a967725f 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1409,7 +1409,7 @@ namespace MediaBrowser.Controller.Entities if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0)) { realChildren = realChildren - .OrderBy(e => e.ProductionYear ?? int.MaxValue) + .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue) .ToArray(); } From 29b3aa854310c150e23ec27a41d912fd6fde3c7d Mon Sep 17 00:00:00 2001 From: Ziyuan Qu Date: Fri, 21 Nov 2025 23:14:30 -0500 Subject: [PATCH 034/131] Add hidden file check in bdInfo (#15582) --- MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs index 7c0be5a9f6..dc20a6d631 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using BDInfo.IO; @@ -58,6 +59,8 @@ public class BdInfoDirectoryInfo : IDirectoryInfo } } + private static bool IsHidden(ReadOnlySpan name) => name.StartsWith('.'); + /// /// Gets the directories. /// @@ -65,6 +68,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo public IDirectoryInfo[] GetDirectories() { return _fileSystem.GetDirectories(_impl.FullName) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoDirectoryInfo(_fileSystem, x)) .ToArray(); } @@ -76,6 +80,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo public IFileInfo[] GetFiles() { return _fileSystem.GetFiles(_impl.FullName) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } @@ -88,6 +93,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo public IFileInfo[] GetFiles(string searchPattern) { return _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } @@ -105,6 +111,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo new[] { searchPattern }, false, searchOption == SearchOption.AllDirectories) + .Where(d => !IsHidden(d.Name)) .Select(x => new BdInfoFileInfo(x)) .ToArray(); } From fbb9a0b2c7c5afbc56be76a4eb11a1045f0ab0f0 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 21 Nov 2025 23:14:39 -0500 Subject: [PATCH 035/131] Fix ResolveLinkTarget crashing on exFAT drives (#15568) --- .../IO/FileSystemHelper.cs | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 3e390ca428..44b7fadf5e 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -63,6 +63,29 @@ public static class FileSystemHelper } } + /// + /// Resolves a single link hop for the specified path. + /// + /// + /// Returns null if the path is not a symbolic link or the filesystem does not support link resolution (e.g., exFAT). + /// + /// The file path to resolve. + /// + /// A representing the next link target if the path is a link; otherwise, null. + /// + private static FileInfo? Resolve(string path) + { + try + { + return File.ResolveLinkTarget(path, returnFinalTarget: false) as FileInfo; + } + catch (IOException) + { + // Filesystem doesn't support links (e.g., exFAT). + return null; + } + } + /// /// Gets the target of the specified file link. /// @@ -84,23 +107,26 @@ public static class FileSystemHelper if (!returnFinalTarget) { - return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + return Resolve(linkPath); } - if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) - { - return null; - } - - if (!targetInfo.Exists) + var targetInfo = Resolve(linkPath); + if (targetInfo is null || !targetInfo.Exists) { return targetInfo; } var currentPath = targetInfo.FullName; var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; - while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + + while (true) { + var linkInfo = Resolve(currentPath); + if (linkInfo is null) + { + break; + } + var targetPath = linkInfo.FullName; // If an infinite loop is detected, return the file info for the From daca285568ff39ea4d3d7b3fd5d31e6078f2b86e Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:20:29 +0100 Subject: [PATCH 036/131] Revert "Localization/iso6392.txt: change pob and pop" (#15555) --- Emby.Server.Implementations/Localization/iso6392.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index d5a7e866b8..4ce739c7e0 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -347,8 +347,8 @@ pli||pi|Pali|pali pol||pl|Polish|polonais pon|||Pohnpeian|pohnpei por||pt|Portuguese|portugais -por||pt-pt|Portuguese (Portugal)|portugais (pt-pt) -por||pt-br|Portuguese (Brazil)|portugais (pt-br) +pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt) +pob||pt-br|Portuguese (Brazil)|portugais (pt-br) pra|||Prakrit languages|prâkrit, langues pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) pus||ps|Pushto; Pashto|pachto From 026f7472cbfdd5a99f86fdd076d69168673aeb19 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Sun, 30 Nov 2025 21:38:47 +0800 Subject: [PATCH 037/131] Fix the empty output of trickplay on RK3576 Signed-off-by: nyanmisaka --- .../MediaEncoding/EncodingHelper.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 915c787f29..843590a1f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5949,28 +5949,37 @@ namespace MediaBrowser.Controller.MediaEncoding var isFullAfbcPipeline = isEncoderSupportAfbc && isDrmInDrmOut && !doOclTonemap; var swapOutputWandH = doRkVppTranspose && swapWAndH; - var outFormat = doOclTonemap ? "p010" : (isMjpegEncoder ? "bgra" : "nv12"); // RGA only support full range in rgb fmts + var outFormat = doOclTonemap ? "p010" : "nv12"; var hwScaleFilter = GetHwScaleFilter("vpp", "rkrga", outFormat, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); - var doScaling = GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH); + var doScaling = !string.IsNullOrEmpty(GetHwScaleFilter("vpp", "rkrga", string.Empty, swapOutputWandH, swpInW, swpInH, reqW, reqH, reqMaxW, reqMaxH)); if (!hasSubs || doRkVppTranspose || !isFullAfbcPipeline - || !string.IsNullOrEmpty(doScaling)) + || doScaling) { + var isScaleRatioSupported = IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f); + // RGA3 hardware only support (1/8 ~ 8) scaling in each blit operation, // but in Trickplay there's a case: (3840/320 == 12), enable 2pass for it - if (!string.IsNullOrEmpty(doScaling) - && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f)) + if (doScaling && !isScaleRatioSupported) { // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format. // Use NV15 instead of P010 to avoid the issue. // SDR inputs are using BGRA formats already which is not affected. - var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat; + var intermediateFormat = doOclTonemap ? "nv15" : (isMjpegEncoder ? "bgra" : outFormat); var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_original_aspect_ratio=increase:force_divisible_by=4:afbc=1"; mainFilters.Add(hwScaleFilterFirstPass); } + // The RKMPP MJPEG encoder on some newer chip models no longer supports RGB input. + // Use 2pass here to enable RGA output of full-range YUV in the 2nd pass. + if (isMjpegEncoder && !doOclTonemap && ((doScaling && isScaleRatioSupported) || !doScaling)) + { + var hwScaleFilterFirstPass = "vpp_rkrga=format=bgra:afbc=1"; + mainFilters.Add(hwScaleFilterFirstPass); + } + if (!string.IsNullOrEmpty(hwScaleFilter) && doRkVppTranspose) { hwScaleFilter += $":transpose={transposeDir}"; From 8aff4227d9389d481b5f9c239ef39d11ab256e08 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 30 Nov 2025 09:04:40 -0700 Subject: [PATCH 038/131] Implement caching for OpenAPI document --- .../ApiServiceCollectionExtensions.cs | 5 +- .../Filters/CachingOpenApiProvider.cs | 89 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Server/Filters/CachingOpenApiProvider.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 08c1a5065b..04dd19eda6 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -33,9 +33,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; @@ -259,7 +261,8 @@ namespace Jellyfin.Server.Extensions c.OperationFilter(); c.OperationFilter(); c.DocumentFilter(); - }); + }) + .Replace(ServiceDescriptor.Transient()); } private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs new file mode 100644 index 0000000000..4169f2fb31 --- /dev/null +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -0,0 +1,89 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// +/// OpenApi provider with caching. +/// +internal sealed class CachingOpenApiProvider : ISwaggerProvider +{ + private const string CacheKey = "openapi.json"; + + private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) }; + private static readonly SemaphoreSlim _lock = new(1, 1); + private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1); + + private readonly IMemoryCache _memoryCache; + private readonly SwaggerGenerator _swaggerGenerator; + private readonly SwaggerGeneratorOptions _swaggerGeneratorOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The options accessor. + /// The api descriptions provider. + /// The schema generator. + /// The memory cache. + public CachingOpenApiProvider( + IOptions optionsAccessor, + IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, + ISchemaGenerator schemaGenerator, + IMemoryCache memoryCache) + { + _swaggerGeneratorOptions = optionsAccessor.Value; + _swaggerGenerator = new SwaggerGenerator(_swaggerGeneratorOptions, apiDescriptionsProvider, schemaGenerator); + _memoryCache = memoryCache; + } + + /// + public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null) + { + if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null) + { + return AdjustDocument(openApiDocument, host, basePath); + } + + var acquired = _lock.Wait(_lockTimeout); + try + { + if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null) + { + return AdjustDocument(openApiDocument, host, basePath); + } + + if (!acquired) + { + throw new InvalidOperationException("OpenApi document is generating"); + } + + openApiDocument = _swaggerGenerator.GetSwagger(documentName); + _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions); + return AdjustDocument(openApiDocument, host, basePath); + } + finally + { + if (acquired) + { + _lock.Release(); + } + } + } + + private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath) + { + document.Servers = _swaggerGeneratorOptions.Servers.Count != 0 + ? _swaggerGeneratorOptions.Servers + : string.IsNullOrEmpty(host) && string.IsNullOrEmpty(basePath) + ? [] + : [new OpenApiServer { Url = $"{host}{basePath}" }]; + + return document; + } +} From ba76a8f3ad826b4d52691a969c9c12bc453c3da1 Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Sun, 30 Nov 2025 21:33:32 -0500 Subject: [PATCH 039/131] Bump version to 10.11.4 --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index a5e613cf60..88b4d877fc 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ Jellyfin Contributors Jellyfin.Naming - 10.11.3 + 10.11.4 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index ab049706cc..4c28dfce6b 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ Jellyfin Contributors Jellyfin.Data - 10.11.3 + 10.11.4 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index ef4fdfdb01..0fc74ea56e 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Common - 10.11.3 + 10.11.4 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 737f0c3a3d..d6a5d80d99 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Controller - 10.11.3 + 10.11.4 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 120338d4ee..aec62f9f01 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Model - 10.11.3 + 10.11.4 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/SharedVersion.cs b/SharedVersion.cs index 8e24a20ade..89a5669685 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.3")] -[assembly: AssemblyFileVersion("10.11.3")] +[assembly: AssemblyVersion("10.11.4")] +[assembly: AssemblyFileVersion("10.11.4")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 61421b2259..68e90f00b2 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ Jellyfin Contributors Jellyfin.Extensions - 10.11.3 + 10.11.4 https://github.com/jellyfin/jellyfin GPL-3.0-only From dde70fd8a2007f52f87546eb3c3acf8963333c4c Mon Sep 17 00:00:00 2001 From: myzhysz <4530758+myzhysz@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:02:04 +0800 Subject: [PATCH 040/131] Fix stack overflow while scanning (#15698) --- .../LimitedConcurrencyLibraryScheduler.cs | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs index 0de5f198d7..ccd0b21c56 100644 --- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs +++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs @@ -242,7 +242,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr }; }).ToArray(); - if (ShouldForceSequentialOperation()) + if (ShouldForceSequentialOperation() || _deadlockDetector.Value is not null) { _logger.LogDebug("Process sequentially."); try @@ -267,32 +267,11 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr _tasks.Add(item, CancellationToken.None); } - if (_deadlockDetector.Value is not null) - { - _logger.LogDebug("Nested invocation detected, process in-place."); - try - { - // we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved - while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token)) - { - await ProcessItem(item).ConfigureAwait(false); - } - } - catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested) - { - // operation is cancelled. Do nothing. - } - - _logger.LogDebug("process in-place done."); - } - else - { - Worker(); - _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length); - await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false); - _logger.LogDebug("{NoWorkers} completed.", workItems.Length); - ScheduleTaskCleanup(); - } + Worker(); + _logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length); + await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false); + _logger.LogDebug("{NoWorkers} completed.", workItems.Length); + ScheduleTaskCleanup(); } /// From 2a0b90e3852edae22d9f7cec197e6e81e9415632 Mon Sep 17 00:00:00 2001 From: martenumberto Date: Thu, 4 Dec 2025 03:02:39 +0100 Subject: [PATCH 041/131] Fix: Add .ts fallback for video streams to prevent crash (#15690) --- CONTRIBUTORS.md | 1 + Jellyfin.Api/Helpers/StreamingHelpers.cs | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0a4114478f..8081163ad1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -205,6 +205,7 @@ - [theshoeshiner](https://github.com/theshoeshiner) - [TokerX](https://github.com/TokerX) - [GeneMarks](https://github.com/GeneMarks) + - [martenumberto](https://github.com/martenumberto) # Emby Contributors diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 2601fa3be8..b3f5b9a801 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -159,6 +159,13 @@ public static class StreamingHelpers string? containerInternal = Path.GetExtension(state.RequestedUrl); + if (string.IsNullOrEmpty(containerInternal) + && (!string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId) + || (mediaSource != null && mediaSource.IsInfiniteStream))) + { + containerInternal = ".ts"; + } + if (!string.IsNullOrEmpty(streamingRequest.Container)) { containerInternal = streamingRequest.Container; From fb65f8f8532fbad22089a3a3cfb4d9237c71c567 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 3 Dec 2025 21:02:55 -0500 Subject: [PATCH 042/131] Fix ItemAdded event triggering when updating metadata (#15680) --- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 43f0746ba7..f8e2aece1f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -721,8 +721,6 @@ namespace MediaBrowser.Providers.Manager } } } - - _libraryManager.CreateItem(item, null); } /// From d32f487e8e4762bba740b586285b663712eda69a Mon Sep 17 00:00:00 2001 From: Ivan Kara Date: Thu, 4 Dec 2025 09:04:59 +0700 Subject: [PATCH 043/131] Fix symlinked file size (#15681) --- Jellyfin.Api/Controllers/LibraryController.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4c9cc2b1e8..7566e03eb7 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -23,6 +23,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Activity; @@ -700,7 +701,18 @@ public class LibraryController : BaseJellyfinApiController // Quotes are valid in linux. They'll possibly cause issues here. var filename = Path.GetFileName(item.Path)?.Replace("\"", string.Empty, StringComparison.Ordinal); - return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), filename, true); + var filePath = item.Path; + if (item.IsFileProtocol) + { + // PhysicalFile does not work well with symlinks at the moment. + var resolved = FileSystemHelper.ResolveLinkTarget(filePath, returnFinalTarget: true); + if (resolved is not null && resolved.Exists) + { + filePath = resolved.FullName; + } + } + + return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), filename, true); } /// From ca33bcebf0f557f297cffd12c87a4b186492f9d4 Mon Sep 17 00:00:00 2001 From: Noah Potash Date: Fri, 28 Nov 2025 11:08:15 -0500 Subject: [PATCH 044/131] Add SapientGuardian to CONTRIBUTORS.md --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8081163ad1..a1ba8f17a0 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -117,6 +117,7 @@ - [sachk](https://github.com/sachk) - [sammyrc34](https://github.com/sammyrc34) - [samuel9554](https://github.com/samuel9554) + - [SapientGuardian](https://github.com/SapientGuardian) - [scheidleon](https://github.com/scheidleon) - [sebPomme](https://github.com/sebPomme) - [SegiH](https://github.com/SegiH) From c5147341e3ca7af67907c457794afa0420ec70c1 Mon Sep 17 00:00:00 2001 From: Noah Potash Date: Fri, 28 Nov 2025 11:08:53 -0500 Subject: [PATCH 045/131] Fixes 15661. Replace BlockingCollection with Channel in LimitedConcurrencyLibraryScheduler to prevent blocking in an asynchronous context. --- .../LimitedConcurrencyLibraryScheduler.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs index ccd0b21c56..2811a081aa 100644 --- a/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs +++ b/MediaBrowser.Controller/LibraryTaskScheduler/LimitedConcurrencyLibraryScheduler.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using Microsoft.Extensions.Hosting; @@ -29,7 +30,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr /// private readonly Lock _taskLock = new(); - private readonly BlockingCollection _tasks = new(); + private readonly Channel _tasks = Channel.CreateUnbounded(); private volatile int _workCounter; private Task? _cleanupTask; @@ -77,7 +78,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr lock (_taskLock) { - if (_tasks.Count > 0 || _workCounter > 0) + if (_tasks.Reader.Count > 0 || _workCounter > 0) { _logger.LogDebug("Delay cleanup task, operations still running."); // tasks are still there so its still in use. Reschedule cleanup task. @@ -144,9 +145,9 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr _deadlockDetector.Value = stopToken.TaskStop; try { - foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token)) + while (!stopToken.GlobalStop.Token.IsCancellationRequested) { - stopToken.GlobalStop.Token.ThrowIfCancellationRequested(); + var item = await _tasks.Reader.ReadAsync(stopToken.GlobalStop.Token).ConfigureAwait(false); try { var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0; @@ -264,7 +265,7 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr for (var i = 0; i < workItems.Length; i++) { var item = workItems[i]!; - _tasks.Add(item, CancellationToken.None); + await _tasks.Writer.WriteAsync(item, CancellationToken.None).ConfigureAwait(false); } Worker(); @@ -283,13 +284,12 @@ public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibr } _disposed = true; - _tasks.CompleteAdding(); + _tasks.Writer.Complete(); foreach (var item in _taskRunners) { await item.Key.CancelAsync().ConfigureAwait(false); } - _tasks.Dispose(); if (_cleanupTask is not null) { await _cleanupTask.ConfigureAwait(false); From 997362fc97ddbd9af5e0b5b613c7e6a2e3a28d84 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 6 Dec 2025 03:27:30 +0100 Subject: [PATCH 046/131] Backport dependency updates (#15723) --- .config/dotnet-tools.json | 2 +- .github/workflows/ci-codeql-analysis.yml | 10 ++-- .github/workflows/ci-compat.yml | 16 +++--- .github/workflows/ci-openapi.yml | 24 ++++---- .github/workflows/ci-tests.yml | 6 +- .github/workflows/commands.yml | 6 +- .github/workflows/issue-stale.yml | 2 +- .github/workflows/issue-template-check.yml | 4 +- .github/workflows/pull-request-stale.yaml | 2 +- .github/workflows/release-bump-version.yaml | 4 +- Directory.Packages.props | 62 ++++++++++----------- 11 files changed, 69 insertions(+), 69 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index df2b50e269..029a48f6a1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.10", + "version": "9.0.11", "commands": [ "dotnet-ef" ] diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 9a4c95e26c..1a0e8e8d7a 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -20,18 +20,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 + uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index a8104a917d..8a755a3172 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -11,13 +11,13 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: abi-head retention-days: 14 @@ -40,14 +40,14 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 7cca2af274..0a391dbe1b 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -16,18 +16,18 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: openapi-head retention-days: 14 @@ -41,7 +41,7 @@ jobs: permissions: read-all steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} @@ -55,13 +55,13 @@ jobs: ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: openapi-base retention-days: 14 @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: openapi-base path: openapi-base @@ -158,7 +158,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: openapi-head path: openapi-head @@ -172,7 +172,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (unstable) into place - uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 + uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: openapi-head path: openapi-head @@ -234,7 +234,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (stable) into place - uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 + uses: appleboy/ssh-action@823bd89e131d8d508129f9443cad5855e9ba96f0 # v1.2.4 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 846835491a..f70243221d 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -20,9 +20,9 @@ jobs: runs-on: "${{ matrix.os }}" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + - uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1 with: dotnet-version: ${{ env.SDK_VERSION }} @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17 + uses: danielpalme/ReportGenerator-GitHub-Action@ee0ae774f6d3afedcbd1683c1ab21b83670bdf8e # v5.5.1 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index ba12d47473..0d3e09d1a1 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -24,7 +24,7 @@ jobs: reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 @@ -40,11 +40,11 @@ jobs: runs-on: ubuntu-latest steps: - name: pull in script - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.14' cache: 'pip' diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index db22848c3f..cb535297e0 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index b49647d337..8be48b5c3a 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -10,11 +10,11 @@ jobs: issues: write steps: - name: pull in script - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.14' cache: 'pip' diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index 223ffc590b..0d74e643e2 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml index ec91744f32..d39d2cb9c3 100644 --- a/.github/workflows/release-bump-version.yaml +++ b/.github/workflows/release-bump-version.yaml @@ -33,7 +33,7 @@ jobs: yq-version: v4.9.8 - name: Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ env.TAG_BRANCH }} @@ -66,7 +66,7 @@ jobs: NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} steps: - name: Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ env.TAG_BRANCH }} diff --git a/Directory.Packages.props b/Directory.Packages.props index dc3e7d7bca..7afc4aa763 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + @@ -17,7 +17,7 @@ - + @@ -26,33 +26,33 @@ - - + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -62,13 +62,13 @@ - + - + @@ -84,11 +84,11 @@ - - - + + + - + From 636908fc4dc4cd69a1c20949a5f7c6cba25de67a Mon Sep 17 00:00:00 2001 From: liszto Date: Sat, 6 Dec 2025 03:29:54 +0100 Subject: [PATCH 047/131] Fix thumbnails not being deleted from temp folder --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 86 ++++++++++++++++-------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 503e2f941f..c6eab92ead 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -209,39 +209,69 @@ public class SkiaEncoder : IImageEncoder return default; } - using var codec = SKCodec.Create(safePath, out var result); - - switch (result) + SKCodec? codec = null; + bool isSafePathTemp = !string.Equals(Path.GetFullPath(safePath), Path.GetFullPath(path), StringComparison.OrdinalIgnoreCase); + try { - case SKCodecResult.Success: - // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel - // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.) - // `SKCodec.Create` returns a *non‑null* codec together with - // SKCodecResult.InternalError. The header still contains valid dimensions, - // which is all we need here – so we fall back to them instead of aborting. - // See e.g. Skia bugs #4139, #6092. - case SKCodecResult.InternalError when codec is not null: - var info = codec.Info; - return new ImageDimensions(info.Width, info.Height); - - case SKCodecResult.Unimplemented: - _logger.LogDebug("Image format not supported: {FilePath}", path); - return default; - - default: + codec = SKCodec.Create(safePath, out var result); + switch (result) { - var boundsInfo = SKBitmap.DecodeBounds(safePath); + case SKCodecResult.Success: + // Skia/SkiaSharp edge‑case: when the image header is parsed but the actual pixel + // decode fails (truncated JPEG/PNG, exotic ICC/EXIF, CMYK without color‑transform, etc.) + // `SKCodec.Create` returns a *non‑null* codec together with + // SKCodecResult.InternalError. The header still contains valid dimensions, + // which is all we need here – so we fall back to them instead of aborting. + // See e.g. Skia bugs #4139, #6092. + case SKCodecResult.InternalError when codec is not null: + var info = codec.Info; + return new ImageDimensions(info.Width, info.Height); - if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + case SKCodecResult.Unimplemented: + _logger.LogDebug("Image format not supported: {FilePath}", path); + return default; + + default: { - return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); - } + var boundsInfo = SKBitmap.DecodeBounds(safePath); + if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + { + return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + } - _logger.LogWarning( - "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", - path, - result); - return default; + _logger.LogWarning( + "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", + path, + result); + + return default; + } + } + } + finally + { + try + { + codec?.Dispose(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Error by closing codec for {FilePath}", safePath); + } + + if (isSafePathTemp) + { + try + { + if (File.Exists(safePath)) + { + File.Delete(safePath); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Unable to remove temporary file '{TempPath}'", safePath); + } } } } From 4c5a3fbff34a603ff0344e0b42d07bc17f31f92c Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 6 Dec 2025 10:30:02 +0800 Subject: [PATCH 048/131] Use original name for MusicAritist matching (#15689) --- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + .../Item/BaseItemRepository.cs | 11 +++++++++-- .../Entities/InternalItemsQuery.cs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cab87e53de..83c4eb2e91 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1058,6 +1058,7 @@ namespace Emby.Server.Implementations.Library { IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, + UseRawName = true, DtoOptions = options }).Cast() .OrderBy(i => i.IsAccessedByName ? 1 : 0) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 84168291a8..2d2e5c277d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1930,8 +1930,15 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.Name)) { - var cleanName = GetCleanValue(filter.Name); - baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + if (filter.UseRawName == true) + { + baseQuery = baseQuery.Where(e => e.Name == filter.Name); + } + else + { + var cleanName = GetCleanValue(filter.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } } // These are the same, for now diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index b32b64f5da..076a592922 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities public string? Name { get; set; } + public bool? UseRawName { get; set; } + public string? Person { get; set; } public Guid[] PersonIds { get; set; } From 2e8d9a311b57c171b43ec999adf0f94a8fd6a177 Mon Sep 17 00:00:00 2001 From: Collin Swisher Date: Mon, 8 Dec 2025 17:41:48 -0600 Subject: [PATCH 049/131] Fix case sensitivity edge case --- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index b534ccd1bd..2add65d321 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -149,7 +149,7 @@ namespace Jellyfin.Server.Implementations.Users ThrowIfInvalidUsername(newName); - if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)) + if (user.Username.Equals(newName, StringComparison.Ordinal)) { throw new ArgumentException("The new and old names must be different."); } From ef7f138a4e53bb241dc558f9800f5cd2cc3e35aa Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 9 Dec 2025 14:21:09 -0500 Subject: [PATCH 050/131] Fix trickplay images using wrong item on alternate versions --- Jellyfin.Api/Controllers/TrickplayController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index 2cf66144ce..c9f8b36768 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -86,7 +86,7 @@ public class TrickplayController : BaseJellyfinApiController [FromRoute, Required] int index, [FromQuery] Guid? mediaSourceId) { - var item = _libraryManager.GetItemById(itemId, User.GetUserId()); + var item = _libraryManager.GetItemById(mediaSourceId ?? itemId, User.GetUserId()); if (item is null) { return NotFound(); From 5804d6840c0276d3aef81bfec6af82e496672f01 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 13 Dec 2025 10:25:48 -0500 Subject: [PATCH 051/131] Fix parental rating comparison with sub-scores (#15786) --- MediaBrowser.Controller/Entities/BaseItem.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 3c46d53e5c..d9d2d0e3a8 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1620,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - if (maxAllowedSubRating is not null) + if (!maxAllowedRating.HasValue) { - return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value; + return true; } - return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value; + if (ratingScore.Score != maxAllowedRating.Value) + { + return ratingScore.Score < maxAllowedRating.Value; + } + + return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value; } public ParentalRatingScore GetParentalRatingScore() From 22da5187c88a60118cac03bc77427efa72b97888 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 13 Dec 2025 10:27:01 -0500 Subject: [PATCH 052/131] Fix collection display order (#15767) --- MediaBrowser.Controller/Entities/Folder.cs | 7 ------- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 8 +++++++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 59a967725f..d2a3290c47 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1406,13 +1406,6 @@ namespace MediaBrowser.Controller.Entities .Where(e => query is null || UserViewBuilder.FilterItem(e, query)) .ToArray(); - if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0)) - { - realChildren = realChildren - .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue) - .ToArray(); - } - var childCount = realChildren.Length; if (result.Count < limit) { diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 1d1fb2c392..3999c3e076 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies if (sortBy == ItemSortBy.Default) { - return items; + return items; } return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); @@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies return Sort(children, user).ToArray(); } + public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null) + { + var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query); + return Sort(children, user).ToArray(); + } + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { var children = base.GetRecursiveChildren(user, query, out totalCount); From 035b5895b051edf3f8bb653e52555fb3d63f3544 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 13 Dec 2025 23:27:29 +0800 Subject: [PATCH 053/131] Fix AV1 decoding hang regression on RK3588 (#15776) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 843590a1f4..e088cd358d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -7039,8 +7039,8 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)) { - var accelType = GetHwaccelType(state, options, "av1", bitDepth, hwSurface); - return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty); + // there's an issue about AV1 AFBC on RK3588, disable it for now until it's fixed upstream + return GetHwaccelType(state, options, "av1", bitDepth, hwSurface); } } From b617c62f8ef53848d136155a02e9d3fbffc7b365 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 13 Dec 2025 10:28:31 -0500 Subject: [PATCH 054/131] Fix NullReferenceException in ApplyOrder method (#15768) --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 2d2e5c277d..b8804689ec 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1529,14 +1529,14 @@ public sealed class BaseItemRepository private IQueryable ApplyOrder(IQueryable query, InternalItemsQuery filter, JellyfinDbContext context) { - var orderBy = filter.OrderBy; + var orderBy = filter.OrderBy.Where(e => e.OrderBy != ItemSortBy.Default).ToArray(); var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm); if (hasSearch) { - orderBy = filter.OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy]; + orderBy = [(ItemSortBy.SortName, SortOrder.Ascending), .. orderBy]; } - else if (orderBy.Count == 0) + else if (orderBy.Length == 0) { return query.OrderBy(e => e.SortName); } From 12c5d6b63650c34a25609066a0138d37032eb7c2 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 13 Dec 2025 10:29:17 -0500 Subject: [PATCH 055/131] Fix backdrop images being deleted when stored with media (#15766) --- .../Manager/ItemImageProvider.cs | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 75882a088a..e0354dbdfa 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -88,7 +88,15 @@ namespace MediaBrowser.Providers.Manager } } - singular.AddRange(item.GetImages(ImageType.Backdrop)); + foreach (var backdrop in item.GetImages(ImageType.Backdrop)) + { + var imageInMetadataFolder = backdrop.Path.StartsWith(itemMetadataPath, StringComparison.OrdinalIgnoreCase); + if (imageInMetadataFolder || canDeleteLocal || item.IsSaveLocalMetadataEnabled()) + { + singular.Add(backdrop); + } + } + PruneImages(item, singular); return singular.Count > 0; @@ -466,10 +474,36 @@ namespace MediaBrowser.Providers.Manager } } - if (UpdateMultiImages(item, images, ImageType.Backdrop)) + bool hasBackdrop = false; + bool backdropStoredWithMedia = false; + + foreach (var image in images) { - changed = true; - foundImageTypes.Add(ImageType.Backdrop); + if (image.Type != ImageType.Backdrop) + { + continue; + } + + hasBackdrop = true; + + if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase)) + { + backdropStoredWithMedia = true; + break; + } + } + + if (hasBackdrop) + { + if (UpdateMultiImages(item, images, ImageType.Backdrop)) + { + changed = true; + } + + if (backdropStoredWithMedia) + { + foundImageTypes.Add(ImageType.Backdrop); + } } if (foundImageTypes.Count > 0) From 6e60634c9f078cc01e343b07a0a6b2a5c230478c Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 13 Dec 2025 16:39:49 +0100 Subject: [PATCH 056/131] Skip invalid ignore rules (#15746) --- .../Library/DotIgnoreIgnoreRule.cs | 48 +++++++++- .../Library/DotIgnoreIgnoreRuleTest.cs | 87 +++++++++++++++---- 2 files changed, 115 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 473ff8e1d7..ef5d24c70f 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -70,12 +71,55 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule { // If file has content, base ignoring off the content .gitignore-style rules var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return CheckIgnoreRules(path, rules, isDirectory); + } + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory) + => CheckIgnoreRules(path, rules, isDirectory, IsWindows); + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// Whether to normalize backslashes to forward slashes (for Windows paths). + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath) + { var ignore = new Ignore.Ignore(); - ignore.Add(rules); + + // Add each rule individually to catch and skip invalid patterns + var validRulesAdded = 0; + foreach (var rule in rules) + { + try + { + ignore.Add(rule); + validRulesAdded++; + } + catch (RegexParseException) + { + // Ignore invalid patterns + } + } + + // If no valid rules were added, fall back to ignoring everything (like an empty .ignore file) + if (validRulesAdded == 0) + { + return true; + } // Mitigate the problem of the Ignore library not handling Windows paths correctly. // See https://github.com/jellyfin/jellyfin/issues/15484 - var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + var pathToCheck = normalizePath ? path.NormalizePath('/') : path; // Add trailing slash for directories to match "folder/" if (isDirectory) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index d677c9f091..a7bbef7ed4 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,30 +1,81 @@ +using Emby.Server.Implementations.Library; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; public class DotIgnoreIgnoreRuleTest { - [Fact] - public void Test() + private static readonly string[] _rule1 = ["SPs"]; + private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"]; + private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"]; + private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"]; + + public static TheoryData CheckIgnoreRulesTestData => + new() + { + // Basic pattern matching + { _rule1, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule1, "cd/sps/ffffff.mkv", false, true }, + { _rule1, "/cd/sps/ffffff.mkv", false, true }, + + // Negate pattern + { _rule2, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule2, "cd/sps/ffffff.mkv", false, true }, + { _rule2, "/cd/sps/ffffff.mkv", false, true }, + { _rule2, "f:/cd/sps/thebestshot.mkv", false, false }, + { _rule2, "cd/sps/thebestshot.mkv", false, false }, + { _rule2, "/cd/sps/thebestshot.mkv", false, false }, + + // Mixed valid and invalid patterns - skips invalid, applies valid + { _rule3, "test.txt", false, true }, + { _rule3, "videos/movie.mp4", false, true }, + { _rule3, "movie.mkv", false, true }, + { _rule3, "test.mp3", false, false }, + + // Only invalid patterns - falls back to ignore all + { _rule4, "any-file.txt", false, true }, + { _rule4, "any/path/to/file.mkv", false, true }, + }; + + public static TheoryData WindowsPathNormalizationTestData => + new() + { + // Windows paths with backslashes - should match when normalizePath is true + { _rule1, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule1, @"D:\media\sps\movie.mkv", false, true }, + { _rule1, @"\\server\share\sps\file.mkv", false, true }, + + // Negate pattern with Windows paths + { _rule2, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule2, @"C:\cd\sps\thebestshot.mkv", false, false }, + + // Directory matching with Windows paths + { _rule3, @"C:\videos\movie.mp4", false, true }, + { _rule3, @"D:\documents\test.txt", false, true }, + { _rule3, @"E:\music\song.mp3", false, false }, + }; + + [Theory] + [MemberData(nameof(CheckIgnoreRulesTestData))] + public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory)); } - [Fact] - public void TestNegatePattern() + [Theory] + [MemberData(nameof(WindowsPathNormalizationTestData))] + public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - ignore.Add("!thebestshot.mkv"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); - Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv")); + // With normalizePath=true, backslashes should be converted to forward slashes + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true)); + } + + [Theory] + [InlineData(@"C:\cd\sps\ffffff.mkv")] + [InlineData(@"D:\media\sps\movie.mkv")] + public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path) + { + // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes + Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } } From 4cdd8c8233cc8e2b4ced9be5b7ddbd48f190a3b9 Mon Sep 17 00:00:00 2001 From: Andrew Rabert <6550543+andrewrabert@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:58:08 -0500 Subject: [PATCH 057/131] Fix unnecessary database JOINs in ApplyNavigations (#15666) --- .../Item/BaseItemRepository.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b8804689ec..289ead11d7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -410,10 +410,25 @@ public sealed class BaseItemRepository private static IQueryable ApplyNavigations(IQueryable dbQuery, InternalItemsQuery filter) { - dbQuery = dbQuery.Include(e => e.TrailerTypes) - .Include(e => e.Provider) - .Include(e => e.LockedFields) - .Include(e => e.UserData); + if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + { + dbQuery = dbQuery.Include(e => e.TrailerTypes); + } + + if (filter.DtoOptions.ContainsField(ItemFields.ProviderIds)) + { + dbQuery = dbQuery.Include(e => e.Provider); + } + + if (filter.DtoOptions.ContainsField(ItemFields.Settings)) + { + dbQuery = dbQuery.Include(e => e.LockedFields); + } + + if (filter.DtoOptions.EnableUserData) + { + dbQuery = dbQuery.Include(e => e.UserData); + } if (filter.DtoOptions.EnableImages) { From 1e27f460fe741429a1200b3ceb524d34e5d524ce Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Sun, 14 Dec 2025 21:44:14 -0500 Subject: [PATCH 058/131] Bump version to 10.11.5 --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 88b4d877fc..3d4f3d9f4d 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ Jellyfin Contributors Jellyfin.Naming - 10.11.4 + 10.11.5 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 4c28dfce6b..41429d9619 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ Jellyfin Contributors Jellyfin.Data - 10.11.4 + 10.11.5 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 0fc74ea56e..b046b53b1d 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Common - 10.11.4 + 10.11.5 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index d6a5d80d99..cf13d4d87e 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Controller - 10.11.4 + 10.11.5 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index aec62f9f01..7959bc240f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Model - 10.11.4 + 10.11.5 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/SharedVersion.cs b/SharedVersion.cs index 89a5669685..de59b5d80a 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.4")] -[assembly: AssemblyFileVersion("10.11.4")] +[assembly: AssemblyVersion("10.11.5")] +[assembly: AssemblyFileVersion("10.11.5")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 68e90f00b2..d366d666d8 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ Jellyfin Contributors Jellyfin.Extensions - 10.11.4 + 10.11.5 https://github.com/jellyfin/jellyfin GPL-3.0-only From 2ccf08f547346d81de39fac5b3ff680d49f15d28 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 17 Dec 2025 01:07:36 -0500 Subject: [PATCH 059/131] Fix artist display order --- .../Item/BaseItemRepository.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 289ead11d7..f477d8aa8a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2629,6 +2629,12 @@ public sealed class BaseItemRepository .Where(e => artistNames.Contains(e.Name)) .ToArray(); - return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast().ToArray()); + var lookup = artists + .GroupBy(e => e.Name!) + .ToDictionary( + g => g.Key, + g => g.Select(f => DeserializeBaseItem(f)).Cast().ToArray()); + + return artistNames.Where(lookup.ContainsKey).ToDictionary(name => name, name => lookup[name]); } } From f2d0ac7b28b7c26accedff5a368d158a119fdc70 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Fri, 19 Dec 2025 20:33:24 +0800 Subject: [PATCH 060/131] Fix missing H.264 and AV1 SDR fallbacks in HLS playlist Previously, if HEVC encoding was disabled on the server, SDR fallbacks would not be provided. Signed-off-by: nyanmisaka --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 112 +++++++++++++---------- 1 file changed, 63 insertions(+), 49 deletions(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index a38ad379cc..16e51151d9 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -154,7 +154,7 @@ public class DynamicHlsHelper // from universal audio service, need to override the AudioCodec when the actual request differs from original query if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase)) { - var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString()); + var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString); newQuery["AudioCodec"] = state.OutputAudioCodec; queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery); } @@ -173,10 +173,21 @@ public class DynamicHlsHelper queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons; } - // Main stream - var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + // Video rotation metadata is only supported in fMP4 remuxing + if (state.VideoStream is not null + && state.VideoRequest is not null + && (state.VideoStream?.Rotation ?? 0) != 0 + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) + && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) + { + queryString += "&AllowVideoStreamCopy=false"; + } - playlistUrl += queryString; + // Main stream + var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8"; + var playlistUrl = baseUrl + queryString; + var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString); var subtitleStreams = state.MediaSource .MediaStreams @@ -198,37 +209,36 @@ public class DynamicHlsHelper AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); } - // Video rotation metadata is only supported in fMP4 remuxing - if (state.VideoStream is not null - && state.VideoRequest is not null - && (state.VideoStream?.Rotation ?? 0) != 0 - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) - && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)) - { - playlistUrl += "&AllowVideoStreamCopy=false"; - } - var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); if (state.VideoStream is not null && state.VideoRequest is not null) { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - // Provide SDR HEVC entrance for backward compatibility. - if (encodingOptions.AllowHevcEncoding - && !encodingOptions.AllowAv1Encoding - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) - && state.VideoStream.VideoRange == VideoRange.HDR - && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + // Provide AV1 and HEVC SDR entrances for backward compatibility. + foreach (var sdrVideoCodec in new[] { "av1", "hevc" }) { - var requestedVideoProfiles = state.GetRequestedProfiles("hevc"); - if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0) + var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding + && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase); + var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding + && string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase); + var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed; + + if (isEncodingAllowed + && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.VideoRange == VideoRange.HDR) { - // Force HEVC Main Profile and disable video stream copy. - state.OutputVideoCodec = "hevc"; - var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main"); - sdrVideoUrl += "&AllowVideoStreamCopy=false"; + // Force AV1 and HEVC Main Profile and disable video stream copy. + state.OutputVideoCodec = sdrVideoCodec; + + var sdrPlaylistQuery = playlistQuery; + sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec; + sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main"; + sdrPlaylistQuery["AllowVideoStreamCopy"] = "false"; + + var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery); // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range. AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup); @@ -238,12 +248,30 @@ public class DynamicHlsHelper } } + // Provide H.264 SDR entrance for backward compatibility. + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + && state.VideoStream.VideoRange == VideoRange.HDR) + { + // Force H.264 and disable video stream copy. + state.OutputVideoCodec = "h264"; + + var sdrPlaylistQuery = playlistQuery; + sdrPlaylistQuery["VideoCodec"] = "h264"; + sdrPlaylistQuery["AllowVideoStreamCopy"] = "false"; + + var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery); + + // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range. + AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup); + + // Restore the video codec + state.OutputVideoCodec = "copy"; + } + // Provide Level 5.0 entrance for backward compatibility. // e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video, // but in fact it is capable of playing videos up to Level 6.1. - if (encodingOptions.AllowHevcEncoding - && !encodingOptions.AllowAv1Encoding - && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) + if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.VideoStream.Level.HasValue && state.VideoStream.Level > 150 && state.VideoStream.VideoRange == VideoRange.SDR @@ -273,12 +301,15 @@ public class DynamicHlsHelper var variation = GetBitrateVariation(totalBitrate); var newBitrate = totalBitrate - variation; - var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + var variantQuery = playlistQuery; + variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture); + var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); variation *= 2; newBitrate = totalBitrate - variation; - variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); + variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture); + variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); } @@ -863,23 +894,6 @@ public class DynamicHlsHelper return variation; } - private string ReplaceVideoBitrate(string url, int oldValue, int newValue) - { - return url.Replace( - "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), - "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), - StringComparison.OrdinalIgnoreCase); - } - - private string ReplaceProfile(string url, string codec, string oldValue, string newValue) - { - string profileStr = codec + "-profile="; - return url.Replace( - profileStr + oldValue, - profileStr + newValue, - StringComparison.OrdinalIgnoreCase); - } - private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue) { var oldPlaylist = playlist.ToString(); From 18096e48e0c72b08598a06e5512e6eb81d91fb51 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 20 Dec 2025 10:53:28 +0800 Subject: [PATCH 061/131] Use hvc1 codectag for Dolby Vision 8.4 (#15835) --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index fe6f855b5e..1e3e2740f0 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1839,8 +1839,9 @@ public class DynamicHlsController : BaseJellyfinApiController { if (isActualOutputVideoCodecHevc) { - // Prefer dvh1 to dvhe - args += " -tag:v:0 dvh1 -strict -2"; + // Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari. + var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1"; + args += $" -tag:v:0 {codecTag} -strict -2"; } else if (isActualOutputVideoCodecAv1) { From 9470439cfa1eaf7cb9717f16031b020cedab516a Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 20 Dec 2025 10:54:48 +0800 Subject: [PATCH 062/131] Fix video lacking SAR and DAR are marked as anamorphic (#15834) --- .../Probing/ProbeResultNormalizer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index eb312029a1..8758d71851 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -853,7 +853,12 @@ namespace MediaBrowser.MediaEncoding.Probing } // http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe - if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) + if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio) + && string.IsNullOrEmpty(streamInfo.DisplayAspectRatio)) + { + stream.IsAnamorphic = false; + } + else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal)) { stream.IsAnamorphic = false; } From 8379b4634aeaf9827d07a41cf9ba8fd80c8c323e Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 20 Dec 2025 10:57:08 +0800 Subject: [PATCH 063/131] Enforce more strict webm check (#15807) --- .../Probing/ProbeResultNormalizer.cs | 9 +- .../Probing/ProbeResultNormalizerTests.cs | 12 ++ .../video_web_like_mkv_with_subtitle.json | 137 ++++++++++++++++++ 3 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 8758d71851..55662e4013 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -299,9 +299,12 @@ namespace MediaBrowser.MediaEncoding.Probing // Handle WebM else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase)) { - // Limit WebM to supported codecs - if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) - || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) + // Limit WebM to supported stream types and codecs. + // FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible. + // Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result. + if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio) + || mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)) + || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase)))) { splitFormat[i] = string.Empty; } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 94710a0957..8a2f84734e 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -195,6 +195,18 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.False(res.MediaStreams[0].IsAVC); } + [Fact] + public void GetMediaInfo_WebM_Like_Mkv() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_web_like_mkv_with_subtitle.json"); + var internalMediaInfoResult = JsonSerializer.Deserialize(bytes, _jsonOptions); + + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File); + + Assert.Equal("mkv", res.Container); + Assert.Equal(3, res.MediaStreams.Count); + } + [Fact] public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success() { diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json new file mode 100644 index 0000000000..4f52dd90dc --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json @@ -0,0 +1,137 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "vp8", + "codec_long_name": "On2 VP8", + "profile": "1", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 540, + "height": 360, + "coded_width": 540, + "coded_height": 360, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "3:2", + "pix_fmt": "yuv420p", + "level": -99, + "field_order": "progressive", + "refs": 1, + "r_frame_rate": "2997/125", + "avg_frame_rate": "2997/125", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 1, + "codec_name": "vorbis", + "codec_long_name": "Vorbis", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "44100", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "duration": "117.707000", + "bit_rate": "127998", + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 2, + "codec_name": "subrip", + "codec_long_name": "SubRip subtitle", + "codec_type": "subtitle", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + } + ], + "format": { + "filename": "sample.mkv", + "nb_streams": 3, + "nb_programs": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "117.700914", + "size": "8566268", + "bit_rate": "582239", + "probe_score": 100 + } +} From 4c587776d6263698bd0e00b56c06f14d46c4c2ec Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 20 Dec 2025 10:58:56 +0800 Subject: [PATCH 064/131] Fix the use of HWA in unsupported H.264 Hi422P/Hi444PP (#15819) --- .../MediaEncoding/EncodingHelper.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index e088cd358d..91d88dc08b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6359,6 +6359,21 @@ namespace MediaBrowser.Controller.MediaEncoding } } + // Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format + if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)) + { + if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase)) + { + // VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P + if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox + && RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))) + { + return null; + } + } + } + var decoder = hardwareAccelerationType switch { HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth), From 1805f2259f44aba0ca97ff0de2ad0b0a3614fa03 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Sat, 20 Dec 2025 04:38:54 +0100 Subject: [PATCH 065/131] add CultureDto cache (#15826) --- .../Localization/LocalizationManager.cs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index b4c65ad85f..b3d6d95bb1 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -38,6 +38,7 @@ namespace Emby.Server.Implementations.Localization private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly ConcurrentDictionary _cultureCache = new(StringComparer.OrdinalIgnoreCase); private List _cultures = []; private FrozenDictionary _iso6392BtoT = null!; @@ -161,6 +162,7 @@ namespace Emby.Server.Implementations.Localization list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames)); } + _cultureCache.Clear(); _cultures = list; _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } @@ -169,20 +171,31 @@ namespace Emby.Server.Implementations.Localization /// public CultureDto? FindLanguageInfo(string language) { - // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs - for (var i = 0; i < _cultures.Count; i++) + if (string.IsNullOrEmpty(language)) { - var culture = _cultures[i]; - if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) - || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) - || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase) - || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) - { - return culture; - } + return null; } - return default; + return _cultureCache.GetOrAdd( + language, + static (lang, cultures) => + { + // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs + for (var i = 0; i < cultures.Count; i++) + { + var culture = cultures[i]; + if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase) + || lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase) + || culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase) + || lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)) + { + return culture; + } + } + + return null; + }, + _cultures); } /// From 156761405e7fd5308474a7e6301839ae7c694dfa Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 20 Dec 2025 04:41:09 +0100 Subject: [PATCH 066/131] Prefer US rating on fallback (#15793) --- .../Localization/LocalizationManager.cs | 10 +++++++--- .../Localization/LocalizationManagerTests.cs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index b3d6d95bb1..bc80c2b405 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -324,15 +324,19 @@ namespace Emby.Server.Implementations.Localization else { // Fall back to server default language for ratings check - // If it has no ratings, use the US ratings - var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); + var ratingsDictionary = GetParentalRatingsDictionary(); if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { return value; } } - // If we don't find anything, check all ratings systems + // If we don't find anything, check all ratings systems, starting with US + if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue)) + { + return usValue; + } + foreach (var dictionary in _allParentalRatings.Values) { if (dictionary.TryGetValue(rating, out var value)) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 6d6bba4fc4..e60522bf78 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -203,6 +203,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Null(localizationManager.GetRatingScore(value)); } + [Theory] + [InlineData("TV-MA", "DE", 17, 1)] // US-only rating, DE country code + [InlineData("PG-13", "FR", 13, 0)] // US-only rating, FR country code + [InlineData("R", "JP", 17, 0)] // US-only rating, JP country code + public async Task GetRatingScore_FallbackPrioritizesUS_Success(string rating, string countryCode, int expectedScore, int? expectedSubScore) + { + var localizationManager = Setup(new ServerConfiguration() + { + MetadataCountryCode = countryCode + }); + await localizationManager.LoadAll(); + + var score = localizationManager.GetRatingScore(rating); + + Assert.NotNull(score); + Assert.Equal(expectedScore, score.Score); + Assert.Equal(expectedSubScore, score.SubScore); + } + [Theory] [InlineData("Default", "Default")] [InlineData("HeaderLiveTV", "Live TV")] From 78e3702cb064fc664ed1a658ad534cf66f5373d3 Mon Sep 17 00:00:00 2001 From: Collin T Swisher <79892877+Collin-Swish@users.noreply.github.com> Date: Wed, 24 Dec 2025 08:50:15 -0600 Subject: [PATCH 067/131] Fix playlist item de-duplication (#15858) --- MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 8df15e4408..e0a4c4f320 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -72,7 +72,7 @@ public class PlaylistMetadataService : MetadataService } else { - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray(); } if (replaceData || targetItem.Shares.Count == 0) From e4b82025b8cde9948671f26da05fda7915f9b0a4 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:09:53 +0100 Subject: [PATCH 068/131] move reattaching user data to own function and call it only after fetching metadata for the first time --- CONTRIBUTORS.md | 1 + .../Library/LibraryManager.cs | 6 ++++ .../Item/BaseItemRepository.cs | 32 +++++++++++-------- MediaBrowser.Controller/Entities/BaseItem.cs | 2 ++ .../Library/ILibraryManager.cs | 7 ++++ .../Persistence/IItemRepository.cs | 7 ++++ .../Manager/MetadataService.cs | 11 +++++-- 7 files changed, 49 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a1ba8f17a0..3b7d6d0a16 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -207,6 +207,7 @@ - [TokerX](https://github.com/TokerX) - [GeneMarks](https://github.com/GeneMarks) - [martenumberto](https://github.com/martenumberto) + - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) # Emby Contributors diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 83c4eb2e91..83b135f924 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync([item], parent, updateReason, cancellationToken); + /// + public void ReattachUserData(BaseItem item, CancellationToken cancellationToken) + { + _itemRepository.ReattachUserData(item, cancellationToken); + } + public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { if (item.IsFileProtocol) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 289ead11d7..f4c4cb731a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -617,7 +617,6 @@ public sealed class BaseItemRepository var ids = tuples.Select(f => f.Item.Id).ToArray(); var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); - var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray(); foreach (var item in tuples) { @@ -651,19 +650,6 @@ public sealed class BaseItemRepository context.SaveChanges(); - foreach (var item in newItems) - { - // reattach old userData entries - var userKeys = item.UserDataKey.ToArray(); - var retentionDate = (DateTime?)null; - context.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdate(e => e - .SetProperty(f => f.ItemId, item.Item.Id) - .SetProperty(f => f.RetentionDate, retentionDate)); - } - var itemValueMaps = tuples .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) .ToArray(); @@ -759,6 +745,24 @@ public sealed class BaseItemRepository transaction.Commit(); } + /// + public void ReattachUserData(BaseItemDto item, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(item); + cancellationToken.ThrowIfCancellationRequested(); + + using var context = _dbProvider.CreateDbContext(); + + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + context.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdate(e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate)); + } + /// public BaseItemDto? RetrieveItem(Guid id) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d9d2d0e3a8..4938b43e4b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,6 +2053,8 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); + public void ReattachUserData(CancellationToken cancellationToken) => LibraryManager.ReattachUserData(this, cancellationToken); + /// /// Validates that images within the item are still on the filesystem. /// diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index fcc5ed672a..32bacc8dc2 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -281,6 +281,13 @@ namespace MediaBrowser.Controller.Library /// Returns a Task that can be awaited. Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0026ab2b5f..9443dd3f20 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -35,6 +35,13 @@ public interface IItemRepository void SaveImages(BaseItem item); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index a2102ca9cd..5b82b18cc3 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager if (isFirstRefresh) { - await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false); } // Next run metadata providers @@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false); } return updateType; @@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager } } - protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken) { await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); + if (reattachUserData) + { + result.Item.ReattachUserData(cancellationToken); + } + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; From 09a1c31fa303856c8b9724df06f68eb5bb88ea05 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:06:07 +0100 Subject: [PATCH 069/131] Refactor ReattachUserData methods to be asynchronous --- Emby.Server.Implementations/Library/LibraryManager.cs | 4 ++-- .../Item/BaseItemRepository.cs | 10 ++++++---- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++- MediaBrowser.Controller/Library/ILibraryManager.cs | 3 ++- MediaBrowser.Controller/Persistence/IItemRepository.cs | 3 ++- MediaBrowser.Providers/Manager/MetadataService.cs | 2 +- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 83b135f924..1716c49e59 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2203,9 +2203,9 @@ namespace Emby.Server.Implementations.Library => UpdateItemsAsync([item], parent, updateReason, cancellationToken); /// - public void ReattachUserData(BaseItem item, CancellationToken cancellationToken) + public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken) { - _itemRepository.ReattachUserData(item, cancellationToken); + await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false); } public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f4c4cb731a..8191bd02e1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -746,7 +746,7 @@ public sealed class BaseItemRepository } /// - public void ReattachUserData(BaseItemDto item, CancellationToken cancellationToken) + public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(item); cancellationToken.ThrowIfCancellationRequested(); @@ -755,12 +755,14 @@ public sealed class BaseItemRepository var userKeys = item.GetUserDataKeys().ToArray(); var retentionDate = (DateTime?)null; - context.UserData + await context.UserData .Where(e => e.ItemId == PlaceholderId) .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdate(e => e + .ExecuteUpdateAsync( + e => e .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate)); + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4938b43e4b..7586b99e77 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,7 +2053,8 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); - public void ReattachUserData(CancellationToken cancellationToken) => LibraryManager.ReattachUserData(this, cancellationToken); + public async Task ReattachUserDataAsync(CancellationToken cancellationToken) => + await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false); /// /// Validates that images within the item are still on the filesystem. diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 32bacc8dc2..675812ac23 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -286,7 +286,8 @@ namespace MediaBrowser.Controller.Library /// /// The item. /// The cancellation token. - void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); /// /// Retrieves the item. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 9443dd3f20..790efb86a6 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -40,7 +40,8 @@ public interface IItemRepository /// /// The item. /// The cancellation token. - void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); /// /// Retrieves the item. diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 5b82b18cc3..e9cb46eab5 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -280,7 +280,7 @@ namespace MediaBrowser.Providers.Manager await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); if (reattachUserData) { - result.Item.ReattachUserData(cancellationToken); + await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); } if (result.Item.SupportsPeople && result.People is not null) From adaca955901ec2b332dae1cdfa58c79c2ef754b4 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Wed, 31 Dec 2025 07:43:07 +0100 Subject: [PATCH 070/131] make db context creation async --- .../Item/BaseItemRepository.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 8191bd02e1..5d26393111 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -751,18 +751,21 @@ public sealed class BaseItemRepository ArgumentNullException.ThrowIfNull(item); cancellationToken.ThrowIfCancellationRequested(); - using var context = _dbProvider.CreateDbContext(); + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - var userKeys = item.GetUserDataKeys().ToArray(); - var retentionDate = (DateTime?)null; - await context.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdateAsync( - e => e - .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate), - cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + } } /// From 559e0088e5316a857f764a848e76e4fbd62fa834 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 4 Jan 2026 13:20:34 -0500 Subject: [PATCH 071/131] Fix tag inheritance for Continue Watching queries (#15931) --- .../Item/BaseItemRepository.cs | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 289ead11d7..8ac7366bec 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2447,35 +2447,24 @@ public sealed class BaseItemRepository if (filter.ExcludeInheritedTags.Length > 0) { + var excludedTags = filter.ExcludeInheritedTags; baseQuery = baseQuery.Where(e => - !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)) - && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue || - !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)))); + !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue)) + && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue)))); } if (filter.IncludeInheritedTags.Length > 0) { - // For seasons and episodes, we also need to check the parent series' tags. - if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season)) - { - baseQuery = baseQuery.Where(e => - e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)))); - } + var includeTags = filter.IncludeInheritedTags; + var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist; + baseQuery = baseQuery.Where(e => + e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)) - // A playlist should be accessible to its owner regardless of allowed tags. - else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist) - { - baseQuery = baseQuery.Where(e => - e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)) - || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")); - // d ^^ this is stupid it hate this. - } - else - { - baseQuery = baseQuery.Where(e => - e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))); - } + // For seasons and episodes, we also need to check the parent series' tags. + || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))) + + // A playlist should be accessible to its owner regardless of allowed tags + || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""))); } if (filter.SeriesStatuses.Length > 0) From c86f6439c5fe3f17c015dc5fbdb46ee68162ab25 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 5 Jan 2026 11:06:25 -0500 Subject: [PATCH 072/131] Revert "always sort season by index number" This reverts commit e16ea7b23696a49b96bcd9a8e81cd23db470524b. --- MediaBrowser.Controller/Entities/TV/Series.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 427c2995bc..6396631f99 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; query.IncludeItemTypes = new[] { BaseItemKind.Season }; - query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) }; + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; if (user is not null && !user.DisplayMissingEpisodes) { @@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; + if (query.OrderBy.Count == 0) + { + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; + } if (query.IncludeItemTypes.Length == 0) { From 845b8cdc8f807753f98d38f736800d276f3dc89a Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 6 Jan 2026 11:57:25 -0500 Subject: [PATCH 073/131] Fix crash when plugin repository has an invalid URL --- Emby.Server.Implementations/Updates/InstallationManager.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 5ff4001601..5f9e29b563 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates _logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest); return Array.Empty(); } + catch (NotSupportedException ex) + { + _logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest); + return Array.Empty(); + } catch (HttpRequestException ex) { _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest); From 2cb7fb52d2221d9daa39206089b578c2c0fcb549 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 16 Jan 2026 20:45:19 -0500 Subject: [PATCH 074/131] Skip hidden directories and .ignore paths in library monitoring (#16029) --- Emby.Server.Implementations/IO/LibraryMonitor.cs | 6 ++++++ Emby.Server.Implementations/Library/IgnorePatterns.cs | 1 + .../Library/IgnorePatternsTests.cs | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index d87ad729ee..7cff2a25b6 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO return; } + var fileInfo = _fileSystem.GetFileSystemInfo(path); + if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null)) + { + return; + } + // Ignore certain files, If the parent of an ignored path has a change event, ignore that too foreach (var i in _tempIgnoredPaths.Keys) { diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index fe3a1ce611..5fac2f6b0a 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", + "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 07061cfc77..4cb6cb9607 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", false)] + [InlineData("/media/.hiddendir/file.mp4", true)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } From 22d593b8e986ecdb42fb1e618bfcf833b0a6f118 Mon Sep 17 00:00:00 2001 From: Collin T Swisher <79892877+Collin-Swish@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:47:04 -0600 Subject: [PATCH 075/131] Add mblink creation logic to library update endpoint. (#15965) --- .../Library/LibraryManager.cs | 33 +++++++++++-------- .../Controllers/LibraryStructureController.cs | 11 +++++++ .../Library/ILibraryManager.cs | 7 ++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 1716c49e59..aa5b37a94d 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3201,19 +3201,7 @@ namespace Emby.Server.Implementations.Library var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); - var shortcutFilename = Path.GetFileNameWithoutExtension(path); - - var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - - while (File.Exists(lnk)) - { - shortcutFilename += "1"; - lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); - } - - _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); - - RemoveContentTypeOverrides(path); + CreateShortcut(virtualFolderPath, pathInfo); if (saveLibraryOptions) { @@ -3378,5 +3366,24 @@ namespace Emby.Server.Implementations.Library return item is UserRootFolder || item.IsVisibleStandalone(user); } + + public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo) + { + var path = pathInfo.Path; + var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; + + var shortcutFilename = Path.GetFileNameWithoutExtension(path); + + var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); + + while (File.Exists(lnk)) + { + shortcutFilename += "1"; + lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); + } + + _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path)); + RemoveContentTypeOverrides(path); + } } } diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 2a885662b5..117811429a 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController return NotFound(); } + LibraryOptions options = item.GetLibraryOptions(); + foreach (var mediaPath in request.LibraryOptions!.PathInfos) + { + if (options.PathInfos.Any(i => i.Path == mediaPath.Path)) + { + continue; + } + + _libraryManager.CreateShortcut(item.Path, mediaPath); + } + item.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 675812ac23..df1c98f3f7 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -660,5 +660,12 @@ namespace MediaBrowser.Controller.Library /// This exists so plugins can trigger a library scan. /// void QueueLibraryScan(); + + /// + /// Add mblink file for a media path. + /// + /// The path to the virtualfolder. + /// The new virtualfolder. + public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo); } } From 49775b1f6aaa958f19a0ee4ea05bb9aab78c6b5b Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 16 Jan 2026 20:47:40 -0500 Subject: [PATCH 076/131] Fix birthplace not saving correctly (#16020) --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e2867ffad5..43b88fac8a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -875,7 +875,7 @@ public sealed class BaseItemRepository } dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray(); - dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? []; + dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? []; dto.Studios = entity.Studios?.Split('|') ?? []; dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|'); @@ -1037,7 +1037,7 @@ public sealed class BaseItemRepository } entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null; - entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null; + entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null; entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null; entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null; entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields From 093cfc3f3b72a6bea71cb96ced180a9ac257d537 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Fri, 16 Jan 2026 20:51:48 -0500 Subject: [PATCH 077/131] Trim music artist names (#15808) --- Jellyfin.Api/Controllers/ItemUpdateController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index e1d9b6bba0..e8a50666be 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -418,7 +418,7 @@ public class ItemUpdateController : BaseJellyfinApiController { if (item is IHasAlbumArtist hasAlbumArtists) { - hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name); + hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim()); } } @@ -426,7 +426,7 @@ public class ItemUpdateController : BaseJellyfinApiController { if (item is IHasArtist hasArtists) { - hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name); + hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim()); } } From b56de6493f67cd1cdc43b47745ae66908d1aef41 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 17 Jan 2026 03:03:13 +0100 Subject: [PATCH 078/131] Be more strict about PersonType assignments (#15872) --- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 4 +--- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 4 +--- MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs | 7 ++++--- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 414a0a3c9b..2beb34e43b 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -303,9 +303,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index e30c555cb4..f0e159f098 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 1b429039e7..0905a3bdcb 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -120,9 +120,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index f0828e8263..82d4e58384 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -367,9 +367,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV CrewMember = crewMember, PersonType = TmdbUtils.MapCrewToPersonType(crewMember) }) - .Where(entry => - TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) || - TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType)); if (config.HideMissingCrewMembers) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index f5e59a2789..d6e66a0e61 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -70,18 +70,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public static PersonKind MapCrewToPersonType(Crew crew) { if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) - && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase)) + && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Director; } if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) - && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase)) + && crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Producer; } - if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)) + if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase) + && crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Writer; } From a518160a6ff471541b7daae6d54c8b896bb1f2e6 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 17 Jan 2026 03:05:46 +0100 Subject: [PATCH 079/131] Prioritize better matches on search (#15983) --- .../Item/BaseItemRepository.cs | 29 ++++++++++++------- .../Item/OrderMapper.cs | 27 +++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 43b88fac8a..b58b40b601 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1567,29 +1567,36 @@ public sealed class BaseItemRepository IOrderedQueryable? orderedQuery = null; + // When searching, prioritize by match quality: exact match > prefix match > contains + if (hasSearch) + { + orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!)); + } + var firstOrdering = orderBy.FirstOrDefault(); if (firstOrdering != default) { var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context); - if (firstOrdering.SortOrder == SortOrder.Ascending) + if (orderedQuery is null) { - orderedQuery = query.OrderBy(expression); + // No search relevance ordering, start fresh + orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending + ? query.OrderBy(expression) + : query.OrderByDescending(expression); } else { - orderedQuery = query.OrderByDescending(expression); + // Search relevance ordering already applied, chain with ThenBy + orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending + ? orderedQuery.ThenBy(expression) + : orderedQuery.ThenByDescending(expression); } if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName) { - if (firstOrdering.SortOrder is SortOrder.Ascending) - { - orderedQuery = orderedQuery.ThenBy(e => e.Name); - } - else - { - orderedQuery = orderedQuery.ThenByDescending(e => e.Name); - } + orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending + ? orderedQuery.ThenBy(e => e.Name) + : orderedQuery.ThenByDescending(e => e.Name); } } diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index 192ee74996..1ae7cc6c4a 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using Microsoft.EntityFrameworkCore; @@ -68,4 +69,30 @@ public static class OrderMapper _ => e => e.SortName }; } + + /// + /// Creates an expression to order search results by match quality. + /// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3). + /// + /// The search term to match against. + /// An expression that returns an integer representing match quality (lower is better). + public static Expression> MapSearchRelevanceOrder(string searchTerm) + { + var cleanSearchTerm = GetCleanValue(searchTerm); + var searchPrefix = cleanSearchTerm + " "; + return e => + e.CleanName == cleanSearchTerm ? 0 : + e.CleanName!.StartsWith(searchPrefix) ? 1 : + e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3; + } + + private static string GetCleanValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + return value.RemoveDiacritics().ToLowerInvariant(); + } } From a8d1cdefaca1dd0ce3dc6efa63461643e18f6116 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 17 Jan 2026 10:05:45 -0500 Subject: [PATCH 080/131] Address review comments --- .../Item/BaseItemRepository.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f477d8aa8a..600b646023 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2633,8 +2633,17 @@ public sealed class BaseItemRepository .GroupBy(e => e.Name!) .ToDictionary( g => g.Key, - g => g.Select(f => DeserializeBaseItem(f)).Cast().ToArray()); + g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast().ToArray()); - return artistNames.Where(lookup.ContainsKey).ToDictionary(name => name, name => lookup[name]); + var result = new Dictionary(artistNames.Count); + foreach (var name in artistNames) + { + if (lookup.TryGetValue(name, out var artistArray)) + { + result[name] = artistArray; + } + } + + return result; } } From 94edcbd2d1c8130cf728ed7566694e161dd12b39 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 17 Jan 2026 10:10:06 -0500 Subject: [PATCH 081/131] Fix artist ordering DtoServices --- Emby.Server.Implementations/Dto/DtoService.cs | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index c5dc3b054c..b465ae8ee9 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1051,16 +1051,22 @@ namespace Emby.Server.Implementations.Dto // Include artists that are not in the database yet, e.g., just added via metadata editor // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); - dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]) - .Where(e => e.Value.Length > 0) - .Select(i => + var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]); + + var artistItems = new List(hasArtist.Artists.Count); + foreach (var name in hasArtist.Artists) + { + if (!string.IsNullOrWhiteSpace(name) && artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0) { - return new NameGuidPair + artistItems.Add(new NameGuidPair { - Name = i.Key, - Id = i.Value.First().Id - }; - }).Where(i => i is not null).ToArray(); + Name = name, + Id = artists[0].Id + }); + } + } + + dto.ArtistItems = artistItems.ToArray(); } if (item is IHasAlbumArtist hasAlbumArtist) @@ -1085,31 +1091,22 @@ namespace Emby.Server.Implementations.Dto // }) // .ToList(); - dto.AlbumArtists = hasAlbumArtist.AlbumArtists - // .Except(foundArtists, new DistinctNameComparer()) - .Select(i => + var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]); + + var albumArtistItems = new List(hasAlbumArtist.AlbumArtists.Count); + foreach (var name in hasAlbumArtist.AlbumArtists) + { + if (!string.IsNullOrWhiteSpace(name) && albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0) { - // This should not be necessary but we're seeing some cases of it - if (string.IsNullOrEmpty(i)) + albumArtistItems.Add(new NameGuidPair { - return null; - } - - var artist = _libraryManager.GetArtist(i, new DtoOptions(false) - { - EnableImages = false + Name = albumArtists[0].Name, + Id = albumArtists[0].Id }); - if (artist is not null) - { - return new NameGuidPair - { - Name = artist.Name, - Id = artist.Id - }; - } + } + } - return null; - }).Where(i => i is not null).ToArray(); + dto.AlbumArtists = albumArtistItems.ToArray(); } // Add video info From 2943bb6fdd0782e1a2926fd2e584c8f0707abd6c Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 18 Jan 2026 01:51:51 -0500 Subject: [PATCH 082/131] Restore collection folder image refresh --- .../Images/CollectionFolderImageProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 273d356a39..a25373326f 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex); } + + protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image) + { + var age = DateTime.UtcNow - image.DateModified; + return age.TotalDays > 7; + } } } From 2df546af6d33f079f9b1d7a85a9e6b10c09e1fb4 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 18 Jan 2026 18:16:45 -0500 Subject: [PATCH 083/131] Deduplicate using Distinct --- Emby.Server.Implementations/Dto/DtoService.cs | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index b465ae8ee9..b392340f71 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1053,20 +1053,14 @@ namespace Emby.Server.Implementations.Dto // var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]); - var artistItems = new List(hasArtist.Artists.Count); - foreach (var name in hasArtist.Artists) - { - if (!string.IsNullOrWhiteSpace(name) && artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0) - { - artistItems.Add(new NameGuidPair - { - Name = name, - Id = artists[0].Id - }); - } - } - - dto.ArtistItems = artistItems.ToArray(); + dto.ArtistItems = hasArtist.Artists + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct() + .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0 + ? new NameGuidPair { Name = name, Id = artists[0].Id } + : null) + .Where(item => item is not null) + .ToArray(); } if (item is IHasAlbumArtist hasAlbumArtist) @@ -1093,20 +1087,14 @@ namespace Emby.Server.Implementations.Dto var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]); - var albumArtistItems = new List(hasAlbumArtist.AlbumArtists.Count); - foreach (var name in hasAlbumArtist.AlbumArtists) - { - if (!string.IsNullOrWhiteSpace(name) && albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0) - { - albumArtistItems.Add(new NameGuidPair - { - Name = albumArtists[0].Name, - Id = albumArtists[0].Id - }); - } - } - - dto.AlbumArtists = albumArtistItems.ToArray(); + dto.AlbumArtists = hasAlbumArtist.AlbumArtists + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct() + .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0 + ? new NameGuidPair { Name = name, Id = albumArtists[0].Id } + : null) + .Where(item => item is not null) + .ToArray(); } // Add video info From 10662e75e4626be71184db950ce534ab6953be77 Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Sun, 18 Jan 2026 20:02:59 -0500 Subject: [PATCH 084/131] Bump version to 10.11.6 --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 3d4f3d9f4d..5e236bc230 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ Jellyfin Contributors Jellyfin.Naming - 10.11.5 + 10.11.6 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 41429d9619..8425c07631 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ Jellyfin Contributors Jellyfin.Data - 10.11.5 + 10.11.6 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index b046b53b1d..0e9ce7f2d0 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Common - 10.11.5 + 10.11.6 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index cf13d4d87e..04fe870738 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Controller - 10.11.5 + 10.11.6 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 7959bc240f..41ce9fab8c 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Model - 10.11.5 + 10.11.6 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/SharedVersion.cs b/SharedVersion.cs index de59b5d80a..27170e0d12 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.5")] -[assembly: AssemblyFileVersion("10.11.5")] +[assembly: AssemblyVersion("10.11.6")] +[assembly: AssemblyFileVersion("10.11.6")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index d366d666d8..56fbd13ae7 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ Jellyfin Contributors Jellyfin.Extensions - 10.11.5 + 10.11.6 https://github.com/jellyfin/jellyfin GPL-3.0-only From 644327eb762a907328c68ab9f5d61a151cd96897 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 22 Jan 2026 19:39:55 -0500 Subject: [PATCH 085/131] Revert hidden directory ignore pattern (#16077) --- Emby.Server.Implementations/Library/IgnorePatterns.cs | 5 ++++- .../Library/IgnorePatternsTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 5fac2f6b0a..59ccb9e2c7 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -50,6 +50,10 @@ namespace Emby.Server.Implementations.Library "**/lost+found", "**/subs/**", "**/subs", + "**/.snapshots/**", + "**/.snapshots", + "**/.snapshot/**", + "**/.snapshot", // Trickplay files "**/*.trickplay", @@ -83,7 +87,6 @@ namespace Emby.Server.Implementations.Library // Unix hidden files "**/.*", - "**/.*/**", // Mac - if you ever remove the above. // "**/._*", diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index 4cb6cb9607..07061cfc77 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/.hiddendir/file.mp4", false)] [InlineData("/media/dir/.hiddenfile.mp4", true)] [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] @@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R.", false)] [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)] - public void PathIgnored(string path, bool expected) + public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); } From 673f617994da6ff6a45cf428a3ea47de59edc6c5 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 22 Jan 2026 19:40:35 -0500 Subject: [PATCH 086/131] Fix TMDB crew department mapping (#16066) --- MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index d6e66a0e61..bdac57dac8 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// The Jellyfin person type. public static PersonKind MapCrewToPersonType(Crew crew) { - if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase) + if (crew.Department.Equals("directing", StringComparison.OrdinalIgnoreCase) && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase)) { return PersonKind.Director; @@ -82,7 +82,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb } if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase) - && crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase)) + && (crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase) || crew.Job.Equals("screenplay", StringComparison.OrdinalIgnoreCase))) { return PersonKind.Writer; } From 893a849f28b651657b3797d1711da8f696b4120c Mon Sep 17 00:00:00 2001 From: IceStormNG Date: Fri, 23 Jan 2026 01:41:51 +0100 Subject: [PATCH 087/131] Slightly adjust segment length for fractional framerates (#16053) Co-authored-by: Carsten Braun --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 1e3e2740f0..96b4319f18 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1421,10 +1421,20 @@ public class DynamicHlsController : BaseJellyfinApiController cancellationTokenSource.Token) .ConfigureAwait(false); var mediaSourceId = state.BaseRequest.MediaSourceId; + double fps = state.TargetFramerate ?? 0.0f; + int segmentLength = state.SegmentLength * 1000; + + // If framerate is fractional (i.e. 23.976), we need to slightly adjust segment length + if (Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001) + { + double nearestIntFramerate = Math.Ceiling(fps); + segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps)); + } + var request = new CreateMainPlaylistRequest( mediaSourceId is null ? null : Guid.Parse(mediaSourceId), state.MediaPath, - state.SegmentLength * 1000, + segmentLength, state.RunTimeTicks ?? 0, state.Request.SegmentContainer ?? string.Empty, "hls1/main/", From 95d08b264f68a4348d18746543882356465be3b0 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Fri, 23 Jan 2026 01:43:05 +0100 Subject: [PATCH 088/131] Rehydrate cached UserData after reattachment (#16071) --- .../Item/BaseItemRepository.cs | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 787ef67a0c..dc509ebe58 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -755,16 +755,30 @@ public sealed class BaseItemRepository await using (dbContext.ConfigureAwait(false)) { - var userKeys = item.GetUserDataKeys().ToArray(); - var retentionDate = (DateTime?)null; - await dbContext.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdateAsync( - e => e - .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate), - cancellationToken).ConfigureAwait(false); + var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + + // Rehydrate the cached userdata + item.UserData = await dbContext.UserData + .AsNoTracking() + .Where(e => e.ItemId == item.Id) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } } } From 80ba51729485dc67dcaee4a2f817053d702d816f Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 24 Jan 2026 13:48:05 -0500 Subject: [PATCH 089/131] Fix random sort returning duplicate items --- .../Item/BaseItemRepository.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index dc509ebe58..bd4ff4e312 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -295,6 +295,25 @@ public sealed class BaseItemRepository dbQuery = ApplyGroupingFilter(context, dbQuery, filter); dbQuery = ApplyQueryPaging(dbQuery, filter); + + var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random); + if (hasRandomSort) + { + var orderedIds = dbQuery.Select(e => e.Id).ToList(); + if (orderedIds.Count == 0) + { + return Array.Empty(); + } + + var itemsById = ApplyNavigations(context.BaseItems.Where(e => orderedIds.Contains(e.Id)), filter) + .AsEnumerable() + .Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)) + .Where(dto => dto is not null) + .ToDictionary(i => i!.Id); + + return orderedIds.Where(itemsById.ContainsKey).Select(id => itemsById[id]).ToArray()!; + } + dbQuery = ApplyNavigations(dbQuery, filter); return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray(); From d41e302418120c06602aa03ed1f0ca4c481fab2a Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sun, 25 Jan 2026 21:19:38 +0100 Subject: [PATCH 090/131] Fix SessionInfoWebSocketListener not using SessionInfoDto --- .../Session/SessionManager.cs | 3 ++- .../SessionInfoWebSocketListener.cs | 15 +++++++++------ .../Session/ISessionManager.cs | 7 +++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index cf2ca047cf..2eeeecfec0 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1175,7 +1175,8 @@ namespace Emby.Server.Implementations.Session return session; } - private SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) + /// + public SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo) { return new SessionInfoDto { diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index 143d82bac6..db24c97460 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -7,6 +7,7 @@ using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -15,7 +16,7 @@ namespace Jellyfin.Api.WebSocketListeners; /// /// Class SessionInfoWebSocketListener. /// -public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener, WebSocketListenerState> +public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener, WebSocketListenerState> { private readonly ISessionManager _sessionManager; private bool _disposed; @@ -52,24 +53,26 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener /// Task{SystemInfo}. - protected override Task> GetDataToSend() + protected override Task> GetDataToSend() { - return Task.FromResult(_sessionManager.Sessions); + return Task.FromResult(_sessionManager.Sessions.Select(_sessionManager.ToSessionInfoDto)); } /// - protected override Task> GetDataToSendForConnection(IWebSocketConnection connection) + protected override Task> GetDataToSendForConnection(IWebSocketConnection connection) { + var sessions = _sessionManager.Sessions; + // For non-admin users, filter the sessions to only include their own sessions if (connection.AuthorizationInfo?.User is not null && !connection.AuthorizationInfo.IsApiKey && !connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) { var userId = connection.AuthorizationInfo.User.Id; - return Task.FromResult(_sessionManager.Sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId))); + sessions = sessions.Where(s => s.UserId.Equals(userId) || s.ContainsUser(userId)); } - return Task.FromResult(_sessionManager.Sessions); + return Task.FromResult(sessions.Select(_sessionManager.ToSessionInfoDto)); } /// diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 2b3afa1174..c11c65c334 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -350,5 +350,12 @@ namespace MediaBrowser.Controller.Session /// The session id or playsession id. /// Task. Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId); + + /// + /// Gets the dto for session info. + /// + /// The session info. + /// of the session. + SessionInfoDto ToSessionInfoDto(SessionInfo sessionInfo); } } From 9734494eb6bb212fd447981c11d14e3e461b1941 Mon Sep 17 00:00:00 2001 From: endpne Date: Sun, 25 Jan 2026 21:27:43 +0800 Subject: [PATCH 091/131] Fix TMDB image URLs missing size parameter --- MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index fedf345988..abaca65ff3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -518,7 +518,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb return null; } - return _tmDbClient.GetImageUrl(size, path, true).ToString(); + // Use "original" as default size if size is null or empty to prevent malformed URLs + var imageSize = string.IsNullOrEmpty(size) ? "original" : size; + + return _tmDbClient.GetImageUrl(imageSize, path, true).ToString(); } /// From 1b2d9c100abba01f837380ed84d8306e96d66248 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 12 Feb 2026 16:23:37 -0500 Subject: [PATCH 092/131] Skip image checks for empty folders --- .../Images/BaseDynamicImageProvider.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 4874eca8e6..996cd1b3ca 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -267,22 +267,24 @@ namespace Emby.Server.Implementations.Images { var image = item.GetImageInfo(type, 0); - if (image is not null) + if (image is null) { - if (!image.IsLocalFile) - { - return false; - } + return GetItemsWithImages(item).Count is not 0; + } - if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) - { - return false; - } + if (!image.IsLocalFile) + { + return false; + } - if (!HasChangedByDate(item, image)) - { - return false; - } + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) + { + return false; + } + + if (!HasChangedByDate(item, image)) + { + return false; } return true; From 290463fe7b464ad5218a1b5646891be453bcc458 Mon Sep 17 00:00:00 2001 From: David Federman Date: Wed, 11 Feb 2026 23:35:59 -0800 Subject: [PATCH 093/131] Fix migration UNIQUE constraint on BaseItemProviders Deduplicate ProviderIds by ProviderId during MigrateLibraryDb migration to prevent UNIQUE constraint violations when legacy data contains duplicate provider entries for the same item. Fixes #16134 --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index d221d18531..59b7e143c1 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -1163,7 +1163,9 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine Item = null!, ProviderId = e[0], ProviderValue = string.Join('|', e.Skip(1)) - }).ToArray(); + }) + .DistinctBy(e => e.ProviderId) + .ToArray(); } if (reader.TryGetString(index++, out var imageInfos)) From 7bf08daeec0448598782495c0cd176f3607a1a28 Mon Sep 17 00:00:00 2001 From: David Federman Date: Wed, 11 Feb 2026 23:43:09 -0800 Subject: [PATCH 094/131] Reattach user data after removing items during library scan When items are removed during a library scan, their user data is detached to a placeholder. If a replacement item already exists (e.g., a new version of the same episode was added before the old file was deleted), the user data would be stranded in the placeholder because the replacement item's initial ReattachUserDataAsync call happened before the old item was deleted. This fix checks for remaining valid children that share user data keys with removed items and reattaches any detached user data to them. Fixes #16149 --- MediaBrowser.Controller/Entities/Folder.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d2a3290c47..2ecb6cbdff 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities // That's all the new and changed ones - now see if any have been removed and need cleanup var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var shouldRemove = !IsRoot || allowRemoveRoot; + var actuallyRemoved = new List(); // If it's an AggregateFolder, don't remove if (shouldRemove && itemsRemoved.Count > 0) { @@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities { Logger.LogDebug("Removed item: {Path}", item.Path); + actuallyRemoved.Add(item); item.SetParent(null); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); } @@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities { LibraryManager.CreateItems(newItems, this, cancellationToken); } + + // After removing items, reattach any detached user data to remaining children + // that share the same user data keys (eg. same episode replaced with a new file). + if (actuallyRemoved.Count > 0) + { + var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet(); + foreach (var child in validChildren) + { + if (child.GetUserDataKeys().Any(removedKeys.Contains)) + { + await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); + } + } + } } else { From 016636225824d5c2c1e6d71f08f42ec8507cce4c Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 17 Feb 2026 22:56:45 -0500 Subject: [PATCH 095/131] Use BackupDatabase() instead of File.Move in library.db migration --- .../Migrations/Routines/MigrateLibraryDb.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 59b7e143c1..ce41c0ec55 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -465,7 +465,36 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine SqliteConnection.ClearAllPools(); _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); - File.Move(libraryDbPath, libraryDbPath + ".old", true); + var libraryDbBackupPath = libraryDbPath + ".old"; + + if (File.Exists(libraryDbBackupPath)) + { + File.Delete(libraryDbBackupPath); + } + + using (var source = new SqliteConnection($"Filename={libraryDbPath}")) + using (var destination = new SqliteConnection($"Filename={libraryDbBackupPath}")) + { + source.Open(); + destination.Open(); + source.BackupDatabase(destination); + } + + SqliteConnection.ClearAllPools(); + + File.Delete(libraryDbPath); + + var walPath = libraryDbPath + "-wal"; + if (File.Exists(walPath)) + { + File.Delete(walPath); + } + + var shmPath = libraryDbPath + "-shm"; + if (File.Exists(shmPath)) + { + File.Delete(shmPath); + } } private DatabaseMigrationStep GetPreparedDbContext(string operationName) From 5597d8e1a7f227e89dda121c72bea4877203f240 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Wed, 18 Feb 2026 15:11:21 -0500 Subject: [PATCH 096/131] Checkpoint wal --- .../Migrations/Routines/MigrateLibraryDb.cs | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index ce41c0ec55..d48ab704f6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -464,37 +464,18 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine SqliteConnection.ClearAllPools(); - _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); - var libraryDbBackupPath = libraryDbPath + ".old"; - - if (File.Exists(libraryDbBackupPath)) + using (var checkpointConnection = new SqliteConnection($"Filename={libraryDbPath}")) { - File.Delete(libraryDbBackupPath); - } - - using (var source = new SqliteConnection($"Filename={libraryDbPath}")) - using (var destination = new SqliteConnection($"Filename={libraryDbBackupPath}")) - { - source.Open(); - destination.Open(); - source.BackupDatabase(destination); + checkpointConnection.Open(); + using var cmd = checkpointConnection.CreateCommand(); + cmd.CommandText = "PRAGMA wal_checkpoint(TRUNCATE);"; + cmd.ExecuteNonQuery(); } SqliteConnection.ClearAllPools(); - File.Delete(libraryDbPath); - - var walPath = libraryDbPath + "-wal"; - if (File.Exists(walPath)) - { - File.Delete(walPath); - } - - var shmPath = libraryDbPath + "-shm"; - if (File.Exists(shmPath)) - { - File.Delete(shmPath); - } + _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); + File.Move(libraryDbPath, libraryDbPath + ".old", true); } private DatabaseMigrationStep GetPreparedDbContext(string operationName) From afd3c0d9f3523324b87bbb16113c09e1e185233e Mon Sep 17 00:00:00 2001 From: rijads Date: Thu, 19 Feb 2026 15:21:40 +0100 Subject: [PATCH 097/131] Fix subtitle extraxtion caching empty files --- .../Subtitles/SubtitleEncoder.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 88a7bb4b41..61564e9b76 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -321,7 +321,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } @@ -423,9 +423,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } } - else if (!File.Exists(outputPath)) + else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { failed = true; + + try + { + _logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath); + } } if (failed) @@ -499,7 +512,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); - if (File.Exists(outputPath)) + if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0) { releaser.Dispose(); continue; @@ -713,10 +726,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles { foreach (var outputPath in outputPaths) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); failed = true; + + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } + continue; } @@ -755,7 +782,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -857,9 +884,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); } } - else if (!File.Exists(outputPath)) + else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { failed = true; + + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } } if (failed) From aa4f09c799f2869772b6c4443ccf913520ddfecf Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Thu, 19 Feb 2026 23:53:48 -0500 Subject: [PATCH 098/131] Mitigate pull_request_target privilege escalation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hotfix — replaces pull_request_target with pull_request to stop granting write permissions and secrets to fork PRs. Some workflows will break; can be fixed properly later. --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 6 +++--- .github/workflows/commands.yml | 2 +- .github/workflows/project-automation.yml | 2 +- .github/workflows/pull-request-conflict.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 8a755a3172..0041d53da3 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -1,6 +1,6 @@ name: ABI Compatibility on: - pull_request_target: + pull_request: permissions: {} @@ -77,7 +77,7 @@ jobs: pull-requests: write # to create or update comment (peter-evans/create-or-update-comment) name: ABI - Difference - if: ${{ github.event_name == 'pull_request_target' }} + if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest needs: - abi-head diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 0a391dbe1b..968cd07be0 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -5,7 +5,7 @@ on: - master tags: - 'v*' - pull_request_target: + pull_request: permissions: {} @@ -73,7 +73,7 @@ jobs: pull-requests: write # to create or update comment (peter-evans/create-or-update-comment) name: OpenAPI - Difference - if: ${{ github.event_name == 'pull_request_target' }} + if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest needs: - openapi-head @@ -148,7 +148,7 @@ jobs: publish-unstable: name: OpenAPI - Publish Unstable Spec - if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} + if: ${{ github.event_name != 'pull_request' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }} runs-on: ubuntu-latest needs: - openapi-head diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 0d3e09d1a1..0775051f9d 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -4,7 +4,7 @@ on: types: - created - edited - pull_request_target: + pull_request: types: - labeled - synchronize diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index d62f655b30..b509478770 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -4,7 +4,7 @@ on: push: branches: - master - pull_request_target: + pull_request: issue_comment: permissions: {} diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index e6a9bf0caa..b003636a6e 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -4,7 +4,7 @@ on: push: branches: - master - pull_request_target: + pull_request: issue_comment: permissions: {} @@ -16,7 +16,7 @@ jobs: steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 - if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}} + if: ${{ github.event_name == 'push' || github.event_name == 'pull_request'}} with: dirtyLabel: 'merge conflict' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' From 286cc6d720607979148b911336ac516826996876 Mon Sep 17 00:00:00 2001 From: David Federman Date: Sat, 31 Jan 2026 16:15:34 -0800 Subject: [PATCH 099/131] Fix nullref in Season.GetEpisodes when the season is detached from a series --- MediaBrowser.Controller/Entities/TV/Season.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index b972ebaa6b..4360253b01 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -201,12 +201,17 @@ namespace MediaBrowser.Controller.Entities.TV public List GetEpisodes(Series series, User user, IEnumerable allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes) { + if (series is null) + { + return []; + } + return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes); } public List GetEpisodes() { - return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true); + return GetEpisodes(Series, null, null, new DtoOptions(true), true); } public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query) From 33496c16932b115c7f628d8aa3cb52e3522c4874 Mon Sep 17 00:00:00 2001 From: MBR#0001 Date: Sun, 8 Feb 2026 11:10:09 +0100 Subject: [PATCH 100/131] Fix broken library subtitle download settings --- .../FixLibrarySubtitleDownloadLanguages.cs | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs new file mode 100644 index 0000000000..e82123e5ac --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Server.ServerSetupApp; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Globalization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to fix broken library subtitle download languages. +/// +[JellyfinMigration("2026-02-06T20:00:00", nameof(FixLibrarySubtitleDownloadLanguages))] +internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine +{ + private readonly ILocalizationManager _localizationManager; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Localization manager. + /// The startup logger for Startup UI integration. + /// The Library manager. + /// The logger. + public FixLibrarySubtitleDownloadLanguages( + ILocalizationManager localizationManager, + IStartupLogger startupLogger, + ILibraryManager libraryManager, + ILogger logger) + { + _localizationManager = localizationManager; + _libraryManager = libraryManager; + _logger = startupLogger.With(logger); + } + + /// + public Task PerformAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting to fix library subtitle download languages."); + + var virtualFolders = _libraryManager.GetVirtualFolders(false); + + foreach (var virtualFolder in virtualFolders) + { + var options = virtualFolder.LibraryOptions; + if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) + { + continue; + } + + // Some virtual folders don't have a proper item id. + if (!Guid.TryParse(virtualFolder.ItemId, out var folderId)) + { + continue; + } + + var collectionFolder = _libraryManager.GetItemById(folderId); + if (collectionFolder is null) + { + _logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId); + continue; + } + + var fixedLanguages = new List(); + + foreach (var language in options.SubtitleDownloadLanguages) + { + var foundLanguage = _localizationManager.FindLanguageInfo(language)?.ThreeLetterISOLanguageName; + if (foundLanguage is not null) + { + // Converted ISO 639-2/B to T (ger to deu) + if (!string.Equals(foundLanguage, language, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Converted '{Language}' to '{ResolvedLanguage}' in library '{LibraryName}'.", language, foundLanguage, virtualFolder.Name); + } + + if (fixedLanguages.Contains(foundLanguage, StringComparer.OrdinalIgnoreCase)) + { + _logger.LogInformation("Language '{Language}' already exists for library '{LibraryName}'. Skipping duplicate.", foundLanguage, virtualFolder.Name); + continue; + } + + fixedLanguages.Add(foundLanguage); + } + else + { + _logger.LogInformation("Could not resolve language '{Language}' in library '{LibraryName}'. Skipping.", language, virtualFolder.Name); + } + } + + options.SubtitleDownloadLanguages = [.. fixedLanguages]; + collectionFolder.UpdateLibraryOptions(options); + } + + _logger.LogInformation("Library subtitle download languages fixed."); + + return Task.CompletedTask; + } +} From 9cd2418095d756cc5dc099b7e32df75221e6fd69 Mon Sep 17 00:00:00 2001 From: crimsonspecter <246959308+crimsonspecter@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:27:06 +0100 Subject: [PATCH 101/131] Fix: don't apply segment length adjustment for remuxed content --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 96b4319f18..40bd26433e 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1424,8 +1424,8 @@ public class DynamicHlsController : BaseJellyfinApiController double fps = state.TargetFramerate ?? 0.0f; int segmentLength = state.SegmentLength * 1000; - // If framerate is fractional (i.e. 23.976), we need to slightly adjust segment length - if (Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001) + // If video is transcoded and framerate is fractional (i.e. 23.976), we need to slightly adjust segment length + if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && Math.Abs(fps - Math.Floor(fps + 0.001f)) > 0.001) { double nearestIntFramerate = Math.Ceiling(fps); segmentLength = (int)Math.Ceiling(segmentLength * (nearestIntFramerate / fps)); From f34f6b6941d505dec6ec1b0630bb8ec85c19eb96 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 8 Mar 2026 12:46:25 +0100 Subject: [PATCH 102/131] Fix nullref ex in font handling Don't add fallback fonts if they are null The default font can still be null, however unlikely --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 57 +++++++++++++++--------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index c6eab92ead..ade993d927 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -25,7 +25,7 @@ public class SkiaEncoder : IImageEncoder private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private static readonly SKImageFilter _imageFilter; - private static readonly SKTypeface[] _typefaces; + private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); /// /// The default sampling options, equivalent to old high quality filter settings when upscaling. @@ -37,9 +37,7 @@ public class SkiaEncoder : IImageEncoder /// public static readonly SKSamplingOptions DefaultSamplingOptions; -#pragma warning disable CA1810 static SkiaEncoder() -#pragma warning restore CA1810 { var kernel = new[] { @@ -59,21 +57,6 @@ public class SkiaEncoder : IImageEncoder SKShaderTileMode.Clamp, true); - // Initialize the list of typefaces - // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point - // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) - _typefaces = - [ - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic - SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font - ]; - // use cubic for upscaling UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); // use bilinear for everything else @@ -132,7 +115,7 @@ public class SkiaEncoder : IImageEncoder /// /// Gets the default typeface to use. /// - public static SKTypeface DefaultTypeFace => _typefaces.Last(); + public static SKTypeface? DefaultTypeFace => _typefaces.Last(); /// /// Check if the native lib is available. @@ -152,6 +135,40 @@ public class SkiaEncoder : IImageEncoder } } + /// + /// Initialize the list of typefaces + /// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point + /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) + /// + /// The list of typefaces. + private static SKTypeface?[] InitializeTypefaces() + { + int[] chars = [ + '鸡', // CJK Simplified Chinese + '雞', // CJK Traditional Chinese + 'ノ', // CJK Japanese + '각', // CJK Korean + 128169, // Emojis, 128169 is the Pile of Poo (💩) emoji + 'ז', // Hebrew + 'ي' // Arabic + ]; + var fonts = new List(chars.Length + 1); + foreach (var ch in chars) + { + var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch); + if (font is not null) + { + fonts.Add(font); + } + } + + // Default font + fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) + ?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a')); + + return fonts.ToArray(); + } + /// /// Convert a to a . /// @@ -809,7 +826,7 @@ public class SkiaEncoder : IImageEncoder { foreach (var typeface in _typefaces) { - if (typeface.ContainsGlyphs(c)) + if (typeface is not null && typeface.ContainsGlyphs(c)) { return typeface; } From 519d2113eb26840957c978aac7c158a6e41e9fb4 Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Tue, 10 Mar 2026 07:18:22 +0800 Subject: [PATCH 103/131] Fix filter detection in FFmpeg 8.1 Signed-off-by: nyanmisaka --- MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f4e8c39c11..68d6d215b2 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -693,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder [GeneratedRegex("^\\s\\S{6}\\s(?[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] private static partial Regex CodecRegex(); - [GeneratedRegex("^\\s\\S{3}\\s(?[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] + [GeneratedRegex("^\\s\\S{2,3}\\s(?[\\w|-]+)\\s+.+$", RegexOptions.Multiline)] private static partial Regex FilterRegex(); } } From fda49a5a49c2b6eadeb5f9b1b1bb683d536973f3 Mon Sep 17 00:00:00 2001 From: IceStormNG Date: Fri, 13 Mar 2026 20:26:25 +0100 Subject: [PATCH 104/131] Apply analyzeduration and probesize for subtitle streams to improve codec parameter detection (#16293) Apply analyzeduration and probesize for subtitle streams to improve codec parameter detection --- .../MediaEncoding/EncodingHelper.cs | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 91d88dc08b..ffbd6c0565 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1267,6 +1267,20 @@ namespace MediaBrowser.Controller.MediaEncoding } } + // Use analyzeduration also for subtitle streams to improve resolution detection with streams inside MKS files + var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state); + if (!string.IsNullOrEmpty(analyzeDurationArgument)) + { + arg.Append(' ').Append(analyzeDurationArgument); + } + + // Apply probesize, too, if configured + var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg(); + if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument)) + { + arg.Append(' ').Append(ffmpegProbeSizeArgument); + } + // Also seek the external subtitles stream. var seekSubParam = GetFastSeekCommandLineParameter(state, options, segmentContainer); if (!string.IsNullOrEmpty(seekSubParam)) @@ -7118,9 +7132,8 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer) + private string GetFfmpegAnalyzeDurationArg(EncodingJobInfo state) { - var inputModifier = string.Empty; var analyzeDurationArgument = string.Empty; // Apply -analyzeduration as per the environment variable, @@ -7136,6 +7149,26 @@ namespace MediaBrowser.Controller.MediaEncoding analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration; } + return analyzeDurationArgument; + } + + private string GetFfmpegProbesizeArg() + { + var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + + if (!string.IsNullOrEmpty(ffmpegProbeSize)) + { + return $"-probesize {ffmpegProbeSize}"; + } + + return string.Empty; + } + + public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer) + { + var inputModifier = string.Empty; + var analyzeDurationArgument = GetFfmpegAnalyzeDurationArg(state); + if (!string.IsNullOrEmpty(analyzeDurationArgument)) { inputModifier += " " + analyzeDurationArgument; @@ -7144,11 +7177,11 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier = inputModifier.Trim(); // Apply -probesize if configured - var ffmpegProbeSize = _config.GetFFmpegProbeSize(); + var ffmpegProbeSizeArgument = GetFfmpegProbesizeArg(); - if (!string.IsNullOrEmpty(ffmpegProbeSize)) + if (!string.IsNullOrEmpty(ffmpegProbeSizeArgument)) { - inputModifier += $" -probesize {ffmpegProbeSize}"; + inputModifier += " " + ffmpegProbeSizeArgument; } var userAgentParam = GetUserAgentParam(state); From 348b14f7b7ac563824b336e93aa7ce333d652bed Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Mon, 16 Mar 2026 17:58:53 +0800 Subject: [PATCH 105/131] Fix readrate options in FFmpeg 8.1 Signed-off-by: nyanmisaka --- .../MediaEncoding/EncodingHelper.cs | 13 ++++++++++++- src/Jellyfin.LiveTv/IO/EncodedRecorder.cs | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ffbd6c0565..ef591c1258 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -85,6 +85,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); + private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0); private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); @@ -7221,8 +7222,10 @@ namespace MediaBrowser.Controller.MediaEncoding inputModifier += GetVideoSyncOption(state.InputVideoSync, _mediaEncoder.EncoderVersion); } + int readrate = 0; if (state.ReadInputAtNativeFramerate && state.InputProtocol != MediaProtocol.Rtsp) { + readrate = 1; inputModifier += " -re"; } else if (encodingOptions.EnableSegmentDeletion @@ -7233,7 +7236,15 @@ namespace MediaBrowser.Controller.MediaEncoding { // Set an input read rate limit 10x for using SegmentDeletion with stream-copy // to prevent ffmpeg from exiting prematurely (due to fast drive) - inputModifier += " -readrate 10"; + readrate = 10; + inputModifier += $" -readrate {readrate}"; + } + + // Set a larger catchup value to revert to the old behavior, + // otherwise, remuxing might stall due to this new option + if (readrate > 0 && _mediaEncoder.EncoderVersion >= _minFFmpegReadrateCatchupOption) + { + inputModifier += $" -readrate_catchup {readrate * 100}"; } var flags = new List(); diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs index be7ff52977..d877a0d124 100644 --- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs +++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs @@ -156,6 +156,13 @@ namespace Jellyfin.LiveTv.IO if (mediaSource.ReadAtNativeFramerate) { inputModifier += " -re"; + + // Set a larger catchup value to revert to the old behavior, + // otherwise, remuxing might stall due to this new option + if (_mediaEncoder.EncoderVersion >= new Version(8, 0)) + { + inputModifier += " -readrate_catchup 100"; + } } if (mediaSource.RequiresLooping) From e8d72bf6a3c684efe8a4e320d594988e857c68fc Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 16 Mar 2026 10:32:09 -0400 Subject: [PATCH 106/131] Fix restore backup metadata location --- .../FullSystemBackup/BackupService.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 70483c36cc..3a5df4d68d 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -118,15 +118,21 @@ public class BackupService : IBackupService throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version."); } - void CopyDirectory(string source, string target) + void CopyDirectory(string source, string target, string? exclude = null) { var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar); var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar; + var excludePath = exclude is null ? null : $"{source}/{exclude}/"; foreach (var item in zipArchive.Entries) { var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName)); var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName))); + if (excludePath is not null && item.FullName.StartsWith(excludePath, StringComparison.Ordinal)) + { + continue; + } + if (!sourcePath.StartsWith(fullSourcePath, StringComparison.Ordinal) || !targetPath.StartsWith(fullTargetRoot, StringComparison.Ordinal) || Path.EndsInDirectorySeparator(item.FullName)) @@ -142,8 +148,9 @@ public class BackupService : IBackupService } CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath); - CopyDirectory("Data", _applicationPaths.DataPath); + CopyDirectory("Data", _applicationPaths.DataPath, exclude: "metadata"); CopyDirectory("Root", _applicationPaths.RootFolderPath); + CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath); if (manifest.Options.Database) { From 61b19688ff76aa55fe5c39d644c263d72b80199c Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Tue, 17 Mar 2026 23:13:20 -0400 Subject: [PATCH 107/131] Backup default metadata location --- .../FullSystemBackup/BackupService.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs index 3a5df4d68d..5eaf319ab3 100644 --- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs +++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs @@ -118,17 +118,17 @@ public class BackupService : IBackupService throw new NotSupportedException($"The loaded archive '{archivePath}' is made for a newer version of Jellyfin ({manifest.ServerVersion}) and cannot be loaded in this version."); } - void CopyDirectory(string source, string target, string? exclude = null) + void CopyDirectory(string source, string target, string[]? exclude = null) { var fullSourcePath = NormalizePathSeparator(Path.GetFullPath(source) + Path.DirectorySeparatorChar); var fullTargetRoot = Path.GetFullPath(target) + Path.DirectorySeparatorChar; - var excludePath = exclude is null ? null : $"{source}/{exclude}/"; + var excludePaths = exclude?.Select(e => $"{source}/{e}/").ToArray(); foreach (var item in zipArchive.Entries) { var sourcePath = NormalizePathSeparator(Path.GetFullPath(item.FullName)); var targetPath = Path.GetFullPath(Path.Combine(target, Path.GetRelativePath(source, item.FullName))); - if (excludePath is not null && item.FullName.StartsWith(excludePath, StringComparison.Ordinal)) + if (excludePaths is not null && excludePaths.Any(e => item.FullName.StartsWith(e, StringComparison.Ordinal))) { continue; } @@ -148,9 +148,10 @@ public class BackupService : IBackupService } CopyDirectory("Config", _applicationPaths.ConfigurationDirectoryPath); - CopyDirectory("Data", _applicationPaths.DataPath, exclude: "metadata"); + CopyDirectory("Data", _applicationPaths.DataPath, exclude: ["metadata", "metadata-default"]); CopyDirectory("Root", _applicationPaths.RootFolderPath); CopyDirectory("Data/metadata", _applicationPaths.InternalMetadataPath); + CopyDirectory("Data/metadata-default", _applicationPaths.DefaultInternalMetadataPath); if (manifest.Options.Database) { @@ -410,6 +411,15 @@ public class BackupService : IBackupService if (backupOptions.Metadata) { CopyDirectory(Path.Combine(_applicationPaths.InternalMetadataPath), Path.Combine("Data", "metadata")); + + // If a custom metadata path is configured, the default location may still contain data. + if (!string.Equals( + Path.GetFullPath(_applicationPaths.DefaultInternalMetadataPath), + Path.GetFullPath(_applicationPaths.InternalMetadataPath), + StringComparison.OrdinalIgnoreCase)) + { + CopyDirectory(Path.Combine(_applicationPaths.DefaultInternalMetadataPath), Path.Combine("Data", "metadata-default")); + } } var manifestStream = zipArchive.CreateEntry(ManifestEntryName).Open(); From 3d2658fa43b385863efa22b416bda3e504571eec Mon Sep 17 00:00:00 2001 From: Oscar Date: Thu, 19 Mar 2026 22:33:16 +0100 Subject: [PATCH 108/131] Remove -copyts and add -flush_packets 1 to subtitle extraction -copyts is unnecessary for -c:s copy to SRT and slows extraction ~5x. Without -flush_packets 1, ffmpeg buffers all output until exit, leaving .srt files at 0 bytes for minutes while the player shows no subtitles. Fixes #16438 --- MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 61564e9b76..6b88439596 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -569,7 +569,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPaths = new List(); var args = string.Format( CultureInfo.InvariantCulture, - "-i {0} -copyts", + "-i {0}", inputPath); foreach (var subtitleStream in subtitleStreams) @@ -594,7 +594,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", streamIndex, outputCodec, outputPath); @@ -613,7 +613,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var outputPaths = new List(); var args = string.Format( CultureInfo.InvariantCulture, - "-i {0} -copyts", + "-i {0}", inputPath); foreach (var subtitleStream in subtitleStreams) @@ -639,7 +639,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", streamIndex, outputCodec, outputPath); From 4034bf9d7e2cfc8db92ea1fc7f981b6046395be1 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 22 Mar 2026 12:49:33 -0400 Subject: [PATCH 109/131] Save collection id instead of moive id --- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 0217bded13..0757155aac 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -547,7 +547,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); } - if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection)) + if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection)) { writer.WriteElementString("collectionnumber", tmdbCollection); writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString()); From 0581cd661021752e5063e338c718f211c8929310 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 29 Mar 2026 17:22:14 -0400 Subject: [PATCH 110/131] Fix GHSA-j2hf-x4q5-47j3 with improved sanitization Co-Authored-By: Shadowghost --- MediaBrowser.Controller/Entities/BaseItem.cs | 15 +++++++++---- .../MediaInfo/ProbeProvider.cs | 21 ++++++++++++++++++- .../Subtitles/SubtitleManager.cs | 19 ++++++++++++++--- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 7586b99e77..252a5d8b47 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1172,11 +1172,18 @@ namespace MediaBrowser.Controller.Entities info.Video3DFormat = video.Video3DFormat; info.Timestamp = video.Timestamp; - if (video.IsShortcut) + if (video.IsShortcut && !string.IsNullOrEmpty(video.ShortcutPath)) { - info.IsRemote = true; - info.Path = video.ShortcutPath; - info.Protocol = MediaSourceManager.GetPathProtocol(info.Path); + var shortcutProtocol = MediaSourceManager.GetPathProtocol(video.ShortcutPath); + + // Only allow remote shortcut paths — local file paths in .strm files + // could be used to read arbitrary files from the server. + if (shortcutProtocol != MediaProtocol.File) + { + info.IsRemote = true; + info.Path = video.ShortcutPath; + info.Protocol = shortcutProtocol; + } } if (string.IsNullOrEmpty(info.Container)) diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 9f5463b82c..c3ff26202f 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -262,9 +262,28 @@ namespace MediaBrowser.Providers.MediaInfo private void FetchShortcutInfo(BaseItem item) { - item.ShortcutPath = File.ReadAllLines(item.Path) + var shortcutPath = File.ReadAllLines(item.Path) .Select(NormalizeStrmLine) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#')); + + if (string.IsNullOrWhiteSpace(shortcutPath)) + { + return; + } + + // Only allow remote URLs in .strm files to prevent local file access + if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase))) + { + item.ShortcutPath = shortcutPath; + } + else + { + _logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath); + } } /// diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index ae5e1090ad..06556f69b4 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Emby.Naming.Common; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; @@ -32,6 +33,7 @@ namespace MediaBrowser.Providers.Subtitles private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; private readonly ILocalizationManager _localization; + private readonly HashSet _allowedSubtitleFormats; private readonly ISubtitleProvider[] _subtitleProviders; @@ -41,7 +43,9 @@ namespace MediaBrowser.Providers.Subtitles ILibraryMonitor monitor, IMediaSourceManager mediaSourceManager, ILocalizationManager localizationManager, - IEnumerable subtitleProviders) + IEnumerable subtitleProviders, + NamingOptions namingOptions) + { _logger = logger; _fileSystem = fileSystem; @@ -51,6 +55,9 @@ namespace MediaBrowser.Providers.Subtitles _subtitleProviders = subtitleProviders .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) .ToArray(); + _allowedSubtitleFormats = new HashSet( + namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')), + StringComparer.OrdinalIgnoreCase); } /// @@ -171,6 +178,12 @@ namespace MediaBrowser.Providers.Subtitles /// public Task UploadSubtitle(Video video, SubtitleResponse response) { + var format = response.Format; + if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format)) + { + throw new ArgumentException($"Unsupported subtitle format: '{format}'"); + } + var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); return TrySaveSubtitle(video, libraryOptions, response); } @@ -225,7 +238,7 @@ namespace MediaBrowser.Providers.Subtitles foreach (var savePath in savePaths) { - var path = savePath + "." + extension; + var path = Path.GetFullPath(savePath + "." + extension); try { if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal) @@ -236,7 +249,7 @@ namespace MediaBrowser.Providers.Subtitles while (fileExists) { - path = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension); + path = Path.GetFullPath(string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension)); fileExists = File.Exists(path); counter++; } From fddd4e7e6b4de03060d190ac7f332bf34d949ce0 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 29 Mar 2026 17:30:09 -0400 Subject: [PATCH 111/131] Fix GHSA-8fw7-f233-ffr8 with improved sanitization Co-Authored-By: Shadowghost --- Jellyfin.Data/UserEntityExtensions.cs | 2 +- src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs index 149fc9042d..0fc8d3cd25 100644 --- a/Jellyfin.Data/UserEntityExtensions.cs +++ b/Jellyfin.Data/UserEntityExtensions.cs @@ -185,7 +185,7 @@ public static class UserEntityExtensions entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); - entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, false)); entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index 2270758454..5da7762f6f 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts } else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { + if (!IsValidChannelUrl(trimmedLine)) + { + _logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine); + extInf = string.Empty; + continue; + } + var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine); channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); @@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts return numberString; } + private static bool IsValidChannelUrl(string url) + { + return Uri.TryCreate(url, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase)); + } + private static bool IsValidChannelNumber(string numberString) { if (string.IsNullOrWhiteSpace(numberString) From 8d28497d29232805a523abca584f17ecd91b7930 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 29 Mar 2026 18:25:57 -0400 Subject: [PATCH 112/131] Fix lint issue --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index ade993d927..babab57d52 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -138,7 +138,7 @@ public class SkiaEncoder : IImageEncoder /// /// Initialize the list of typefaces /// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point - /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) + /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F). /// /// The list of typefaces. private static SKTypeface?[] InitializeTypefaces() From e0380454949f7000cf56bb6320913864dc0864be Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 29 Mar 2026 19:11:40 -0400 Subject: [PATCH 113/131] Fix lint --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 06556f69b4..58b242ae7e 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -45,7 +45,6 @@ namespace MediaBrowser.Providers.Subtitles ILocalizationManager localizationManager, IEnumerable subtitleProviders, NamingOptions namingOptions) - { _logger = logger; _fileSystem = fileSystem; From d1fd81c38263f4932f28ed24c3042272c901a594 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 30 Mar 2026 09:40:01 +0200 Subject: [PATCH 114/131] Fix GHSA v2jv-54xj-h76w --- Jellyfin.Api/Controllers/SyncPlayController.cs | 2 +- Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 3d6874079d..991fb87144 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -58,7 +58,7 @@ public class SyncPlayController : BaseJellyfinApiController [FromBody, Required] NewGroupRequestDto requestData) { var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); - var syncPlayRequest = new NewGroupRequest(requestData.GroupName); + var syncPlayRequest = new NewGroupRequest(requestData.GroupName.Trim()); return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None)); } diff --git a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs index 32a3bb444c..2e1889fed4 100644 --- a/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs +++ b/Jellyfin.Api/Models/SyncPlayDtos/NewGroupRequestDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace Jellyfin.Api.Models.SyncPlayDtos; /// @@ -17,5 +19,6 @@ public class NewGroupRequestDto /// Gets or sets the group name. /// /// The name of the new group. + [StringLength(200, ErrorMessage = "Group name must not exceed 200 characters.")] public string GroupName { get; set; } } From c0ba29d917ba57ae7251dace4ac4bfb930ea816a Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 30 Mar 2026 04:14:23 -0400 Subject: [PATCH 115/131] fix lint issue --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 58b242ae7e..72c2899a41 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -46,7 +46,6 @@ namespace MediaBrowser.Providers.Subtitles IEnumerable subtitleProviders, NamingOptions namingOptions) { - _logger = logger; _fileSystem = fileSystem; _monitor = monitor; _mediaSourceManager = mediaSourceManager; From e12d93353149ddb24a9af332256c9e375277c9f8 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 30 Mar 2026 04:21:26 -0400 Subject: [PATCH 116/131] Revet lint fix --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 72c2899a41..58b242ae7e 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -46,6 +46,7 @@ namespace MediaBrowser.Providers.Subtitles IEnumerable subtitleProviders, NamingOptions namingOptions) { + _logger = logger; _fileSystem = fileSystem; _monitor = monitor; _mediaSourceManager = mediaSourceManager; From d3907afde7ea0abdf0d3e291b0bd572653bae7ac Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 30 Mar 2026 10:48:51 +0200 Subject: [PATCH 117/131] Add additional validations --- Jellyfin.Api/Controllers/AudioController.cs | 20 +++--- .../Controllers/DynamicHlsController.cs | 62 +++++++++---------- Jellyfin.Api/Controllers/LiveTvController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 4 +- Jellyfin.Api/Controllers/VideosController.cs | 20 +++--- Jellyfin.Api/Helpers/StreamingHelpers.cs | 21 +++++-- .../MediaEncoding/EncodingHelper.cs | 21 ++++--- .../Subtitles/SubtitleManager.cs | 21 +++++-- 8 files changed, 102 insertions(+), 69 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index e334e12640..cd50234160 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 40bd26433e..106eb561f5 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -167,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -208,8 +208,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -416,12 +416,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -453,8 +453,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -592,12 +592,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -628,8 +628,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -762,12 +762,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -799,8 +799,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -934,12 +934,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -970,8 +970,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1107,7 +1107,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1115,12 +1115,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1152,8 +1152,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1292,7 +1292,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1300,12 +1300,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1336,8 +1336,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 10f1789ad8..afbc81c127 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1192,7 +1192,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesVideoFile] public ActionResult GetLiveStreamFile( [FromRoute, Required] string streamId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index fd63347030..8752cb3895 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 97f3239bbc..15dcd14a49 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index b3f5b9a801..1c1d95b587 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -422,14 +422,18 @@ public static class StreamingHelpers request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); break; case 4: - if (videoRequest is not null) + if (videoRequest is not null && IsValidCodecName(val)) { videoRequest.VideoCodec = val; } break; case 5: - request.AudioCodec = val; + if (IsValidCodecName(val)) + { + request.AudioCodec = val; + } + break; case 6: if (videoRequest is not null) @@ -504,7 +508,7 @@ public static class StreamingHelpers break; case 18: - if (videoRequest is not null) + if (videoRequest is not null && IsValidCodecName(val)) { videoRequest.Profile = val; } @@ -563,7 +567,11 @@ public static class StreamingHelpers break; case 30: - request.SubtitleCodec = val; + if (IsValidCodecName(val)) + { + request.SubtitleCodec = val; + } + break; case 31: if (videoRequest is not null) @@ -586,6 +594,11 @@ public static class StreamingHelpers } } + private static bool IsValidCodecName(string val) + { + return EncodingHelper.ContainerValidationRegex().IsMatch(val); + } + /// /// Parses the container into its file extension. /// diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ef591c1258..f483b12192 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -33,12 +33,12 @@ namespace MediaBrowser.Controller.MediaEncoding public partial class EncodingHelper { /// - /// The codec validation regex. + /// The codec validation regex string. /// This regular expression matches strings that consist of alphanumeric characters, hyphens, /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. /// This should matches all common valid codecs. /// - public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + public const string ContainerValidationRegexStr = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; /// /// The level validation regex. @@ -87,8 +87,6 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); private readonly Version _minFFmpegReadrateCatchupOption = new Version(8, 0); - private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); - private static readonly string[] _videoProfilesH264 = [ "ConstrainedBaseline", @@ -181,6 +179,15 @@ namespace MediaBrowser.Controller.MediaEncoding RemoveHdr10Plus, } + /// + /// The codec validation regex. + /// This regular expression matches strings that consist of alphanumeric characters, hyphens, + /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. + /// This should matches all common valid codecs. + /// + [GeneratedRegex(@"^[a-zA-Z0-9\-\._,|]{0,40}$")] + public static partial Regex ContainerValidationRegex(); + [GeneratedRegex(@"\s+")] private static partial Regex WhiteSpaceRegex(); @@ -477,7 +484,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetMjpegEncoder(state, encodingOptions); } - if (_containerValidationRegex.IsMatch(codec)) + if (ContainerValidationRegex().IsMatch(codec)) { return codec.ToLowerInvariant(); } @@ -518,7 +525,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetInputFormat(string container) { - if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container)) + if (string.IsNullOrEmpty(container) || !ContainerValidationRegex().IsMatch(container)) { return null; } @@ -736,7 +743,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = state.OutputAudioCodec; - if (!_containerValidationRegex.IsMatch(codec)) + if (!ContainerValidationRegex().IsMatch(codec)) { codec = "aac"; } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 58b242ae7e..6821e55aab 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -205,7 +205,13 @@ namespace MediaBrowser.Providers.Subtitles } var savePaths = new List(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + var language = response.Language.ToLowerInvariant(); + if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0) + { + throw new ArgumentException("Language contains invalid characters."); + } + + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language; if (response.IsForced) { @@ -233,6 +239,11 @@ namespace MediaBrowser.Providers.Subtitles private async Task TrySaveToFiles(Stream stream, List savePaths, Video video, string extension) { + if (!_allowedSubtitleFormats.Contains("." + extension, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"Invalid subtitle format: {extension}"); + } + List? exs = null; foreach (var savePath in savePaths) @@ -240,15 +251,17 @@ namespace MediaBrowser.Providers.Subtitles var path = Path.GetFullPath(savePath + "." + extension); try { - if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal) - || path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar; + var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar; + if (path.StartsWith(containingFolder, StringComparison.Ordinal) + || path.StartsWith(metadataFolder, StringComparison.Ordinal)) { var fileExists = File.Exists(path); var counter = 0; while (fileExists) { - path = Path.GetFullPath(string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension)); + path = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension); fileExists = File.Exists(path); counter++; } From 2184ed1b162efefde61240f14c10af047818757c Mon Sep 17 00:00:00 2001 From: nyanmisaka Date: Mon, 30 Mar 2026 20:51:11 +0800 Subject: [PATCH 118/131] Fix Null was not checked before using the H264 profile This is rare, but not impossible. Signed-off-by: nyanmisaka --- .../MediaEncoding/EncodingHelper.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ef591c1258..305795a06a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6375,17 +6375,15 @@ namespace MediaBrowser.Controller.MediaEncoding } // Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format - if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) + && ((videoStream.Profile?.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) ?? false) + || (videoStream.Profile?.Contains("4:4:4", StringComparison.OrdinalIgnoreCase) ?? false))) { - if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase) - || videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase)) + // VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P + if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox + && RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))) { - // VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P - if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox - && RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64))) - { - return null; - } + return null; } } From 1932ac4765512a86b6f8304147b65b347c959795 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 30 Mar 2026 18:28:42 +0200 Subject: [PATCH 119/131] Fix CA1810 build error --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 43 ++++++++---------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index babab57d52..3f7ae4d2cd 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -24,44 +24,29 @@ public class SkiaEncoder : IImageEncoder private static readonly HashSet _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private static readonly SKImageFilter _imageFilter; private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); + private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution( + new SKSizeI(3, 3), + [ + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0 + ], + 1f, + 0f, + new SKPointI(1, 1), + SKShaderTileMode.Clamp, + true); /// /// The default sampling options, equivalent to old high quality filter settings when upscaling. /// - public static readonly SKSamplingOptions UpscaleSamplingOptions; + public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); /// /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling. /// - public static readonly SKSamplingOptions DefaultSamplingOptions; - - static SkiaEncoder() - { - var kernel = new[] - { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, - }; - - var kernelSize = new SKSizeI(3, 3); - var kernelOffset = new SKPointI(1, 1); - _imageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, - kernel, - 1f, - 0f, - kernelOffset, - SKShaderTileMode.Clamp, - true); - - // use cubic for upscaling - UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); - // use bilinear for everything else - DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear); - } + public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear); /// /// Initializes a new instance of the class. From 50dc37065b8d530e7dcebc9672dd07583b203582 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 31 Mar 2026 09:30:45 +0200 Subject: [PATCH 120/131] Fix GHSA-jh22-fw8w-2v9x --- Jellyfin.Api/Controllers/AudioController.cs | 4 +- .../Controllers/DynamicHlsController.cs | 14 ++-- Jellyfin.Api/Controllers/VideosController.cs | 4 +- Jellyfin.Api/Helpers/StreamingHelpers.cs | 4 +- .../MediaEncoding/EncodingHelper.cs | 65 ++++++++++--------- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index e334e12640..5d7af1ee1d 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 40bd26433e..4cc4bd9767 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -189,7 +189,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -432,7 +432,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -609,7 +609,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -778,7 +778,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -951,7 +951,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1131,7 +1131,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1317,7 +1317,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 97f3239bbc..ab67682140 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index b3f5b9a801..3b147911a6 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -17,9 +17,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Helpers; @@ -483,7 +481,7 @@ public static class StreamingHelpers request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); break; case 15: - if (videoRequest is not null) + if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val)) { videoRequest.Level = val; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ef591c1258..0a7dc39b0e 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -41,10 +41,10 @@ namespace MediaBrowser.Controller.MediaEncoding public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; /// - /// The level validation regex. + /// The level validation regex string. /// This regular expression matches strings representing a double. /// - public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?"; + public const string LevelValidationRegexStr = @"-?[0-9]+(?:\.[0-9]+)?"; private const string _defaultMjpegEncoder = "mjpeg"; @@ -181,6 +181,9 @@ namespace MediaBrowser.Controller.MediaEncoding RemoveHdr10Plus, } + [GeneratedRegex(@"-?[0-9]+(?:\.[0-9]+)?")] + public static partial Regex LevelValidationRegex(); + [GeneratedRegex(@"\s+")] private static partial Regex WhiteSpaceRegex(); @@ -1783,38 +1786,40 @@ namespace MediaBrowser.Controller.MediaEncoding public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level) { - if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) + if (!double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel)) { - if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + return null; + } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.3 (15) and lower for maximum compatibility. + // https://en.wikipedia.org/wiki/AV1#Levels + if (requestLevel < 0 || requestLevel >= 15) { - // Transcode to level 5.3 (15) and lower for maximum compatibility. - // https://en.wikipedia.org/wiki/AV1#Levels - if (requestLevel < 0 || requestLevel >= 15) - { - return "15"; - } + return "15"; } - else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) - || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + } + else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.0 and lower for maximum compatibility. + // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. + // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels + // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. + if (requestLevel < 0 || requestLevel >= 150) { - // Transcode to level 5.0 and lower for maximum compatibility. - // Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it. - // https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels - // MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880. - if (requestLevel < 0 || requestLevel >= 150) - { - return "150"; - } + return "150"; } - else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + } + else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + // Transcode to level 5.1 and lower for maximum compatibility. + // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. + // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels + if (requestLevel < 0 || requestLevel >= 51) { - // Transcode to level 5.1 and lower for maximum compatibility. - // h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4. - // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels - if (requestLevel < 0 || requestLevel >= 51) - { - return "51"; - } + return "51"; } } @@ -2204,12 +2209,10 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var level = state.GetRequestedLevel(targetVideoCodec); + var level = NormalizeTranscodingLevel(state, state.GetRequestedLevel(targetVideoCodec)); if (!string.IsNullOrEmpty(level)) { - level = NormalizeTranscodingLevel(state, level); - // libx264, QSV, AMF can adjust the given level to match the output. if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) From e8d1d94436f7dd3e21355160f46b4f8ea2a75c57 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 31 Mar 2026 16:35:15 +0200 Subject: [PATCH 121/131] Lock down tuner API to be admin-only --- Jellyfin.Api/Controllers/LiveTvController.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 10f1789ad8..8bbc130d01 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -458,7 +458,7 @@ public class LiveTvController : BaseJellyfinApiController /// A . [HttpPost("Tuners/{tunerId}/Reset")] [ProducesResponseType(StatusCodes.Status204NoContent)] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] public async Task ResetTuner([FromRoute, Required] string tunerId) { await _liveTvManager.ResetTuner(tunerId, CancellationToken.None).ConfigureAwait(false); @@ -983,7 +983,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created tuner host returned. /// A containing the created tuner host. [HttpPost("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); @@ -995,7 +995,7 @@ public class LiveTvController : BaseJellyfinApiController /// Tuner host deleted. /// A . [HttpDelete("TunerHosts")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteTunerHost([FromQuery] string? id) { @@ -1028,7 +1028,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created listings provider returned. /// A containing the created listings provider. [HttpPost("ListingProviders")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA5350:RemoveSha1", MessageId = "AddListingProvider", Justification = "Imported from ServiceStack")] public async Task> AddListingProvider( @@ -1054,7 +1054,7 @@ public class LiveTvController : BaseJellyfinApiController /// Listing provider deleted. /// A . [HttpDelete("ListingProviders")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DeleteListingProvider([FromQuery] string? id) { @@ -1087,7 +1087,7 @@ public class LiveTvController : BaseJellyfinApiController /// Available countries returned. /// A containing the available countries. [HttpGet("ListingProviders/SchedulesDirect/Countries")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() @@ -1108,7 +1108,7 @@ public class LiveTvController : BaseJellyfinApiController /// Channel mapping options returned. /// An containing the channel mapping options. [HttpGet("ChannelMappingOptions")] - [Authorize(Policy = Policies.LiveTvAccess)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task GetChannelMappingOptions([FromQuery] string? providerId) => _listingsManager.GetChannelMappingOptions(providerId); @@ -1120,7 +1120,7 @@ public class LiveTvController : BaseJellyfinApiController /// Created channel mapping returned. /// An containing the created channel mapping. [HttpPost("ChannelMappings")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public Task SetChannelMapping([FromBody, Required] SetChannelMappingDto dto) => _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId); @@ -1144,7 +1144,7 @@ public class LiveTvController : BaseJellyfinApiController /// An containing the tuners. [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")] [HttpGet("Tuners/Discover")] - [Authorize(Policy = Policies.LiveTvManagement)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public IAsyncEnumerable DiscoverTuners([FromQuery] bool newDevicesOnly = false) => _tunerHostManager.DiscoverTuners(newDevicesOnly); @@ -1192,7 +1192,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesVideoFile] public ActionResult GetLiveStreamFile( [FromRoute, Required] string streamId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) From 52aebfb7d3da7a6c3b9e1966b0b16f27e1047d97 Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Tue, 31 Mar 2026 19:33:11 -0400 Subject: [PATCH 122/131] Bump version to 10.11.7 --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 5e236bc230..2b0da1963b 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ Jellyfin Contributors Jellyfin.Naming - 10.11.6 + 10.11.7 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 8425c07631..2bfebaabb7 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ Jellyfin Contributors Jellyfin.Data - 10.11.6 + 10.11.7 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 0e9ce7f2d0..86c9079a8c 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Common - 10.11.6 + 10.11.7 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 04fe870738..f00c56a6ad 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Controller - 10.11.6 + 10.11.7 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 41ce9fab8c..71933341b7 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Model - 10.11.6 + 10.11.7 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/SharedVersion.cs b/SharedVersion.cs index 27170e0d12..659fa3bd21 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.6")] -[assembly: AssemblyFileVersion("10.11.6")] +[assembly: AssemblyVersion("10.11.7")] +[assembly: AssemblyFileVersion("10.11.7")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 56fbd13ae7..488bfaf87a 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ Jellyfin Contributors Jellyfin.Extensions - 10.11.6 + 10.11.7 https://github.com/jellyfin/jellyfin GPL-3.0-only From ff365dae3481390da7aea110246f2065d3987826 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 31 Mar 2026 19:46:47 -0400 Subject: [PATCH 123/131] Fix invalid merge conflict fix --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 9fcbbaefd6..6d73b52ae2 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -485,7 +485,7 @@ public static class StreamingHelpers request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); break; case 15: - if (videoRequest is not null && EncodingHelper.LevelValidationRegex().IsMatch(val)) + if (videoRequest is not null && EncodingHelper.LevelValidationRegexStr().IsMatch(val)) { videoRequest.Level = val; } From b2aa80ce5c1af2a4478d13b50c0961e11a6f5e6b Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 31 Mar 2026 19:59:33 -0400 Subject: [PATCH 124/131] Fix invalid regex comparison --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 6d73b52ae2..e17468cfae 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; @@ -485,7 +486,7 @@ public static class StreamingHelpers request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); break; case 15: - if (videoRequest is not null && EncodingHelper.LevelValidationRegexStr().IsMatch(val)) + if (videoRequest is not null && Regex.IsMatch(val, EncodingHelper.LevelValidationRegexStr)) { videoRequest.Level = val; } From 169e48ac004e48a21d41395baa889a2544af6552 Mon Sep 17 00:00:00 2001 From: MBR#0001 Date: Wed, 14 Jan 2026 19:38:37 +0100 Subject: [PATCH 125/131] Fix querying media with language filters --- .../Item/BaseItemRepository.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index bd4ff4e312..676c194e27 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -33,6 +33,7 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; @@ -69,6 +70,7 @@ public sealed class BaseItemRepository private readonly IItemTypeLookup _itemTypeLookup; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly ILogger _logger; + private readonly ILocalizationManager _localizationManager; private static readonly IReadOnlyList _getAllArtistsValueTypes = [ItemValueType.Artist, ItemValueType.AlbumArtist]; private static readonly IReadOnlyList _getArtistValueTypes = [ItemValueType.Artist]; @@ -85,18 +87,21 @@ public sealed class BaseItemRepository /// The static type lookup. /// The server Configuration manager. /// System logger. + /// Localization manager. public BaseItemRepository( IDbContextFactory dbProvider, IServerApplicationHost appHost, IItemTypeLookup itemTypeLookup, IServerConfigurationManager serverConfigurationManager, - ILogger logger) + ILogger logger, + ILocalizationManager localizationManager) { _dbProvider = dbProvider; _appHost = appHost; _itemTypeLookup = itemTypeLookup; _serverConfigurationManager = serverConfigurationManager; _logger = logger; + _localizationManager = localizationManager; } /// @@ -2297,26 +2302,42 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && f.Language == filter.HasNoAudioTrackWithLanguage)); + var lang = _localizationManager.FindLanguageInfo(filter.HasNoAudioTrackWithLanguage); + if (lang is not null) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio && lang.ThreeLetterISOLanguageNames.Contains(f.Language))); + } } if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && f.Language == filter.HasNoInternalSubtitleTrackWithLanguage)); + var lang = _localizationManager.FindLanguageInfo(filter.HasNoInternalSubtitleTrackWithLanguage); + if (lang is not null) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && !f.IsExternal && lang.ThreeLetterISOLanguageNames.Contains(f.Language))); + } } if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && f.Language == filter.HasNoExternalSubtitleTrackWithLanguage)); + var lang = _localizationManager.FindLanguageInfo(filter.HasNoExternalSubtitleTrackWithLanguage); + if (lang is not null) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.IsExternal && lang.ThreeLetterISOLanguageNames.Contains(f.Language))); + } } if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) { - baseQuery = baseQuery - .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && f.Language == filter.HasNoSubtitleTrackWithLanguage)); + var lang = _localizationManager.FindLanguageInfo(filter.HasNoSubtitleTrackWithLanguage); + if (lang is not null) + { + baseQuery = baseQuery + .Where(e => !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle && lang.ThreeLetterISOLanguageNames.Contains(f.Language))); + } } if (filter.HasSubtitles.HasValue) From 417df3df579fbfb19a960125ce0404dc6f5b68a5 Mon Sep 17 00:00:00 2001 From: MBR#0001 Date: Wed, 1 Apr 2026 16:24:11 +0200 Subject: [PATCH 126/131] Fix subtitle saving --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 6821e55aab..e51826f0ec 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -239,7 +239,8 @@ namespace MediaBrowser.Providers.Subtitles private async Task TrySaveToFiles(Stream stream, List savePaths, Video video, string extension) { - if (!_allowedSubtitleFormats.Contains("." + extension, StringComparison.OrdinalIgnoreCase)) + if (!_allowedSubtitleFormats.Contains("." + extension, StringComparison.OrdinalIgnoreCase) + && !_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException($"Invalid subtitle format: {extension}"); } From ba0720a55533d1c0cd2cd530bdbee893935fa310 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 1 Apr 2026 16:49:48 +0200 Subject: [PATCH 127/131] Handle folders without associated library in FixLibrarySubtitleDownloadLanguages --- .../Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs index e82123e5ac..2b1f549940 100644 --- a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs +++ b/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs @@ -7,7 +7,6 @@ using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -50,7 +49,7 @@ internal class FixLibrarySubtitleDownloadLanguages : IAsyncMigrationRoutine foreach (var virtualFolder in virtualFolders) { var options = virtualFolder.LibraryOptions; - if (options.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) + if (options?.SubtitleDownloadLanguages is null || options.SubtitleDownloadLanguages.Length == 0) { continue; } From cc678383c9d67fa10ff85d22c19c787ce0374aff Mon Sep 17 00:00:00 2001 From: MBR-0001 <55142207+MBR-0001@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:23:18 +0200 Subject: [PATCH 128/131] Simplify subtitle format validation condition --- MediaBrowser.Providers/Subtitles/SubtitleManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index e51826f0ec..a78ec995cf 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -239,8 +239,7 @@ namespace MediaBrowser.Providers.Subtitles private async Task TrySaveToFiles(Stream stream, List savePaths, Video video, string extension) { - if (!_allowedSubtitleFormats.Contains("." + extension, StringComparison.OrdinalIgnoreCase) - && !_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException($"Invalid subtitle format: {extension}"); } From 2c62d40f0d13926874eef9118a95be0dcee4e659 Mon Sep 17 00:00:00 2001 From: Jellyfin Release Bot Date: Sun, 5 Apr 2026 15:08:15 -0400 Subject: [PATCH 129/131] Bump version to 10.11.8 --- Emby.Naming/Emby.Naming.csproj | 2 +- Jellyfin.Data/Jellyfin.Data.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/MediaBrowser.Controller.csproj | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- SharedVersion.cs | 4 ++-- src/Jellyfin.Extensions/Jellyfin.Extensions.csproj | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 2b0da1963b..73d4c10801 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -36,7 +36,7 @@ Jellyfin Contributors Jellyfin.Naming - 10.11.7 + 10.11.8 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 2bfebaabb7..90005acebd 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -18,7 +18,7 @@ Jellyfin Contributors Jellyfin.Data - 10.11.7 + 10.11.8 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 86c9079a8c..1e129919b5 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Common - 10.11.7 + 10.11.8 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index f00c56a6ad..1b66bcbd0a 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Controller - 10.11.7 + 10.11.8 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 71933341b7..28ceeb1915 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -8,7 +8,7 @@ Jellyfin Contributors Jellyfin.Model - 10.11.7 + 10.11.8 https://github.com/jellyfin/jellyfin GPL-3.0-only diff --git a/SharedVersion.cs b/SharedVersion.cs index 659fa3bd21..64fbb58275 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.11.7")] -[assembly: AssemblyFileVersion("10.11.7")] +[assembly: AssemblyVersion("10.11.8")] +[assembly: AssemblyFileVersion("10.11.8")] diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 488bfaf87a..c16c89d875 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -15,7 +15,7 @@ Jellyfin Contributors Jellyfin.Extensions - 10.11.7 + 10.11.8 https://github.com/jellyfin/jellyfin GPL-3.0-only From 97f88743b87f750238b507e0d720e247d13c3b8a Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 27 Apr 2026 20:10:12 +0800 Subject: [PATCH 130/131] Allow HDR10 for VPP tonemapping It mistakenly required OpenCL tone mapping but VPP can handle this --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c9e9341bb5..43227f2eaa 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -413,7 +413,8 @@ namespace MediaBrowser.Controller.MediaEncoding } return state.VideoStream.VideoRange == VideoRange.HDR - && IsDoviWithHdr10Bl(state.VideoStream); + && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 + || IsDoviWithHdr10Bl(state.VideoStream)); } private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options) From 46a53d060567c7dccd20abf211f3956100bfce29 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 28 Apr 2026 00:09:31 +0800 Subject: [PATCH 131/131] also allow hdr10plus --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 43227f2eaa..27d235affa 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -414,6 +414,7 @@ namespace MediaBrowser.Controller.MediaEncoding return state.VideoStream.VideoRange == VideoRange.HDR && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 + || IsHdr10Plus(state.VideoStream) || IsDoviWithHdr10Bl(state.VideoStream)); } @@ -429,8 +430,10 @@ namespace MediaBrowser.Controller.MediaEncoding // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding. // All other HDR formats working. return state.VideoStream.VideoRange == VideoRange.HDR - && (IsDoviWithHdr10Bl(state.VideoStream) - || state.VideoStream.VideoRangeType is VideoRangeType.HLG); + && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 + || IsHdr10Plus(state.VideoStream) + || IsDoviWithHdr10Bl(state.VideoStream) + || state.VideoStream.VideoRangeType == VideoRangeType.HLG); } private bool IsVideoStreamHevcRext(EncodingJobInfo state)