diff options
Diffstat (limited to 'Jellyfin.Server.Implementations/Item')
5 files changed, 93 insertions, 84 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs index 380c6e582c..e4fd3204e1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; -using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -170,92 +169,40 @@ public sealed partial class BaseItemRepository ExcludeItemIds = filter.ExcludeItemIds }; - // Build the master query and collapse rows that share a PresentationUniqueKey - // (e.g. alternate versions) by picking the lowest Id per group. + // Collapse rows that share a PresentationUniqueKey (e.g. alternate versions) by picking + // the lowest Id per group. Keep as an IQueryable sub-select so paging is applied AFTER + // ApplyOrder runs the caller's actual sort. var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter); - - var orderedMasterQuery = ApplyOrder(masterQuery, filter, context) + var representativeIds = masterQuery .GroupBy(e => e.PresentationUniqueKey) .Select(g => g.Min(e => e.Id)); var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); if (filter.EnableTotalRecordCount) { - result.TotalRecordCount = orderedMasterQuery.Count(); + result.TotalRecordCount = representativeIds.Count(); } + var query = ApplyNavigations( + context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => representativeIds.Contains(e.Id)), + filter); + + query = ApplyOrder(query, filter, context); + if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) { - orderedMasterQuery = orderedMasterQuery.Skip(filter.StartIndex.Value); + query = query.Skip(filter.StartIndex.Value); } if (filter.Limit.HasValue) { - orderedMasterQuery = orderedMasterQuery.Take(filter.Limit.Value); + query = query.Take(filter.Limit.Value); } - var masterIds = orderedMasterQuery.ToList(); - - var query = ApplyNavigations( - context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => masterIds.Contains(e.Id)), - filter); - - query = ApplyOrder(query, filter, context); - + result.StartIndex = filter.StartIndex ?? 0; if (filter.IncludeItemTypes.Length > 0) { - var typeSubQuery = new InternalItemsQuery(filter.User) - { - ExcludeItemTypes = filter.ExcludeItemTypes, - IncludeItemTypes = filter.IncludeItemTypes, - MediaTypes = filter.MediaTypes, - AncestorIds = filter.AncestorIds, - ExcludeItemIds = filter.ExcludeItemIds, - ItemIds = filter.ItemIds, - TopParentIds = filter.TopParentIds, - ParentId = filter.ParentId, - IsPlayed = filter.IsPlayed - }; - - var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery) - .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type))); - - var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; - var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; - var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; - var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; - var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; - var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; - var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; - var itemIds = itemCountQuery.Select(e => e.Id); - - // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite) - // Instead, start from ItemValueMaps and join with BaseItems - var countsByCleanName = context.ItemValuesMap - .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type)) - .Where(ivm => itemIds.Contains(ivm.ItemId)) - .Join( - context.BaseItems, - ivm => ivm.ItemId, - e => e.Id, - (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type }) - .GroupBy(x => new { x.CleanName, x.Type }) - .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() }) - .GroupBy(x => x.CleanName) - .ToDictionary( - g => g.Key, - g => new ItemCounts - { - SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count), - EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count), - MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count), - AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count), - ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count), - SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count), - TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count), - }); - - result.StartIndex = filter.StartIndex ?? 0; + var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes); result.Items = [ .. query @@ -273,7 +220,6 @@ public sealed partial class BaseItemRepository } else { - result.StartIndex = filter.StartIndex ?? 0; result.Items = [ .. query @@ -287,4 +233,61 @@ public sealed partial class BaseItemRepository return result; } + + private Dictionary<string, ItemCounts> BuildItemCountsByCleanName( + Database.Implementations.JellyfinDbContext context, + InternalItemsQuery filter, + IReadOnlyList<ItemValueType> itemValueTypes) + { + var typeSubQuery = new InternalItemsQuery(filter.User) + { + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ExcludeItemIds = filter.ExcludeItemIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsPlayed = filter.IsPlayed + }; + + var itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery) + .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type))); + + var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; + var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; + var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; + var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; + var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; + var itemIds = itemCountQuery.Select(e => e.Id); + + // Rewrite query to avoid SelectMany on navigation properties (which requires SQL APPLY, not supported on SQLite) + // Instead, start from ItemValueMaps and join with BaseItems + return context.ItemValuesMap + .Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type)) + .Where(ivm => itemIds.Contains(ivm.ItemId)) + .Join( + context.BaseItems, + ivm => ivm.ItemId, + e => e.Id, + (ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type }) + .GroupBy(x => new { x.CleanName, x.Type }) + .Select(g => new { g.Key.CleanName, g.Key.Type, Count = g.Count() }) + .GroupBy(x => x.CleanName) + .ToDictionary( + g => g.Key, + g => new ItemCounts + { + SeriesCount = g.Where(x => x.Type == seriesTypeName).Sum(x => x.Count), + EpisodeCount = g.Where(x => x.Type == episodeTypeName).Sum(x => x.Count), + MovieCount = g.Where(x => x.Type == movieTypeName).Sum(x => x.Count), + AlbumCount = g.Where(x => x.Type == musicAlbumTypeName).Sum(x => x.Count), + ArtistCount = g.Where(x => x.Type == musicArtistTypeName).Sum(x => x.Count), + SongCount = g.Where(x => x.Type == audioTypeName).Sum(x => x.Count), + TrailerCount = g.Where(x => x.Type == trailerTypeName).Sum(x => x.Count), + }); + } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 0abe981af8..59e61cfd65 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -390,7 +390,8 @@ public sealed partial class BaseItemRepository { if (filter.UseRawName == true) { - baseQuery = baseQuery.Where(e => e.Name == filter.Name); + var nameLower = filter.Name.ToLowerInvariant(); + baseQuery = baseQuery.Where(e => e.Name!.ToLower() == nameLower); } else { diff --git a/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs b/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs index 415510b2f4..9e11b6be62 100644 --- a/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs +++ b/Jellyfin.Server.Implementations/Item/LinkedChildrenService.cs @@ -1,4 +1,6 @@ #pragma warning disable RS0030 // Do not use banned APIs +#pragma warning disable CA1304 // Specify CultureInfo +#pragma warning disable CA1311 // Specify a culture or use an invariant version using System; using System.Collections.Generic; @@ -62,17 +64,19 @@ public class LinkedChildrenService : ILinkedChildrenService { using var dbContext = _dbProvider.CreateDbContext(); + var lowerNames = artistNames.Select(n => n.ToLowerInvariant()).ToArray(); var artists = dbContext.BaseItems .AsNoTracking() .Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!) - .Where(e => artistNames.Contains(e.Name)) + .Where(e => lowerNames.Contains(e.Name!.ToLower())) .ToArray(); var lookup = artists - .GroupBy(e => e.Name!) + .GroupBy(e => e.Name!, StringComparer.OrdinalIgnoreCase) .ToDictionary( g => g.Key, - g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray()); + g => g.Select(f => _queryHelpers.DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray(), + StringComparer.OrdinalIgnoreCase); var result = new Dictionary<string, MusicArtist[]>(artistNames.Count); foreach (var name in artistNames) diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index ada86c8b87..d327b218a9 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -48,9 +48,9 @@ public static class OrderMapper (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.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.Name, _) => e => e.SortName, (ItemSortBy.CommunityRating, _) => e => e.CommunityRating, (ItemSortBy.ProductionYear, _) => e => e.ProductionYear, (ItemSortBy.CriticRating, _) => e => e.CriticRating, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 6cc9729bbe..a0ffe9aea0 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -46,9 +46,10 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I { // The Peoples table has one row per (Name, PersonType), so the same person can // appear multiple times (e.g. as Actor and GuestStar). Collapse to one row per - // name so /Persons doesn't return the same BaseItem id repeatedly. + // name so /Persons doesn't return the same BaseItem id repeatedly. Lowercase the + // grouping key so case-only duplicates collapse together. var representativeIds = dbQuery - .GroupBy(e => e.Name) + .GroupBy(e => e.Name.ToLower()) .Select(g => g.Min(e => e.Id)); dbQuery = context.Peoples.AsNoTracking() .Where(p => representativeIds.Contains(p.Id)) @@ -102,16 +103,16 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I person.Role = person.Role?.Trim() ?? string.Empty; } - // multiple metadata providers can provide the _same_ person - people = people.DistinctBy(e => e.Name + "-" + e.Type).ToArray(); - var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray(); + // multiple metadata providers can provide the _same_ person; dedupe case-insensitively. + people = people.DistinctBy(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray(); + var personKeys = people.Select(e => e.Name.ToLowerInvariant() + "-" + e.Type).ToArray(); using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); var existingPersons = context.Peoples.Select(e => new { item = e, - SelectionKey = e.Name + "-" + e.PersonType + SelectionKey = e.Name.ToLower() + "-" + e.PersonType }) .Where(p => personKeys.Contains(p.SelectionKey)) .Select(f => f.item) @@ -119,7 +120,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I var toAdd = people .Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist) - .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString())) + .Where(e => !existingPersons.Any(f => string.Equals(f.Name, e.Name, StringComparison.OrdinalIgnoreCase) && f.PersonType == e.Type.ToString())) .Select(Map); context.Peoples.AddRange(toAdd); context.SaveChanges(); @@ -137,8 +138,8 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I continue; } - var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString()); - var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role); + var entityPerson = personsEntities.First(e => string.Equals(e.Name, person.Name, StringComparison.OrdinalIgnoreCase) && e.PersonType == person.Type.ToString()); + var existingMap = existingMaps.FirstOrDefault(e => string.Equals(e.People.Name, person.Name, StringComparison.OrdinalIgnoreCase) && e.People.PersonType == person.Type.ToString() && e.Role == person.Role); if (existingMap is null) { context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() |
