diff options
| -rw-r--r-- | Emby.Server.Implementations/Library/Search/SearchManager.cs | 151 | ||||
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs | 187 |
2 files changed, 178 insertions, 160 deletions
diff --git a/Emby.Server.Implementations/Library/Search/SearchManager.cs b/Emby.Server.Implementations/Library/Search/SearchManager.cs index d4c3302239..af916ec9a7 100644 --- a/Emby.Server.Implementations/Library/Search/SearchManager.cs +++ b/Emby.Server.Implementations/Library/Search/SearchManager.cs @@ -145,15 +145,7 @@ public class SearchManager : ISearchManager return candidates; } - var filtered = new List<SearchResult>(allowedIds.Count); - foreach (var c in candidates) - { - if (allowedIds.Contains(c.ItemId)) - { - filtered.Add(c); - } - } - + var filtered = candidates.Where(c => allowedIds.Contains(c.ItemId)).ToList(); if (filtered.Count < candidates.Count) { _logger.LogDebug( @@ -271,46 +263,7 @@ public class SearchManager : ISearchManager break; } - try - { - if (provider is IExternalSearchProvider externalProvider) - { - var count = 0; - await foreach (var result in externalProvider.SearchAsync(providerQuery, cancellationToken).ConfigureAwait(false)) - { - UpdateBestScore(bestScores, result); - count++; - if (bestScores.Count >= requestedLimit) - { - break; - } - } - - _logger.LogDebug( - "External provider {Provider} returned {Count} candidates for search term '{SearchTerm}'", - provider.Name, - count, - searchTerm); - } - else - { - var candidates = await provider.SearchAsync(providerQuery, cancellationToken).ConfigureAwait(false); - foreach (var result in candidates) - { - UpdateBestScore(bestScores, result); - } - - _logger.LogDebug( - "Provider {Provider} returned {Count} candidates for search term '{SearchTerm}'", - provider.Name, - candidates.Count, - searchTerm); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Search provider {Provider} failed for term '{SearchTerm}'", provider.Name, searchTerm); - } + await CollectFromProviderAsync(provider, providerQuery, searchTerm, bestScores, requestedLimit, cancellationToken).ConfigureAwait(false); } return bestScores @@ -320,6 +273,68 @@ public class SearchManager : ISearchManager .ToList(); } + private async Task CollectFromProviderAsync( + ISearchProvider provider, + SearchProviderQuery providerQuery, + string searchTerm, + Dictionary<Guid, float> bestScores, + int requestedLimit, + CancellationToken cancellationToken) + { + try + { + var count = provider is IExternalSearchProvider externalProvider + ? await CollectFromExternalProviderAsync(externalProvider, providerQuery, bestScores, requestedLimit, cancellationToken).ConfigureAwait(false) + : await CollectFromInternalProviderAsync(provider, providerQuery, bestScores, cancellationToken).ConfigureAwait(false); + + _logger.LogDebug( + "Provider {Provider} returned {Count} candidates for search term '{SearchTerm}'", + provider.Name, + count, + searchTerm); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Search provider {Provider} failed for term '{SearchTerm}'", provider.Name, searchTerm); + } + } + + private static async Task<int> CollectFromExternalProviderAsync( + IExternalSearchProvider provider, + SearchProviderQuery providerQuery, + Dictionary<Guid, float> bestScores, + int requestedLimit, + CancellationToken cancellationToken) + { + var count = 0; + await foreach (var result in provider.SearchAsync(providerQuery, cancellationToken).ConfigureAwait(false)) + { + UpdateBestScore(bestScores, result); + count++; + if (bestScores.Count >= requestedLimit) + { + break; + } + } + + return count; + } + + private static async Task<int> CollectFromInternalProviderAsync( + ISearchProvider provider, + SearchProviderQuery providerQuery, + Dictionary<Guid, float> bestScores, + CancellationToken cancellationToken) + { + var candidates = await provider.SearchAsync(providerQuery, cancellationToken).ConfigureAwait(false); + foreach (var result in candidates) + { + UpdateBestScore(bestScores, result); + } + + return candidates.Count; + } + private static void UpdateBestScore(Dictionary<Guid, float> bestScores, SearchResult result) { if (!bestScores.TryGetValue(result.ItemId, out var existingScore) || result.Score > existingScore) @@ -397,42 +412,38 @@ public class SearchManager : ISearchManager private static List<BaseItemKind> BuildIncludeItemTypes(SearchQuery query) { var includeItemTypes = query.IncludeItemTypes.ToList(); - if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Genre))) + if (query.IncludeMedia) { - if (!query.IncludeMedia) - { - AddIfMissing(includeItemTypes, BaseItemKind.Genre); - AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre); - } + return includeItemTypes; } - if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Person))) + if (query.IncludeGenres && IsEmptyOrContains(includeItemTypes, BaseItemKind.Genre)) { - if (!query.IncludeMedia) - { - AddIfMissing(includeItemTypes, BaseItemKind.Person); - } + AddIfMissing(includeItemTypes, BaseItemKind.Genre); + AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre); } - if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Studio))) + if (query.IncludePeople && IsEmptyOrContains(includeItemTypes, BaseItemKind.Person)) { - if (!query.IncludeMedia) - { - AddIfMissing(includeItemTypes, BaseItemKind.Studio); - } + AddIfMissing(includeItemTypes, BaseItemKind.Person); } - if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.MusicArtist))) + if (query.IncludeStudios && IsEmptyOrContains(includeItemTypes, BaseItemKind.Studio)) { - if (!query.IncludeMedia) - { - AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist); - } + AddIfMissing(includeItemTypes, BaseItemKind.Studio); + } + + if (query.IncludeArtists && IsEmptyOrContains(includeItemTypes, BaseItemKind.MusicArtist)) + { + AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist); } return includeItemTypes; } + private static bool IsEmptyOrContains(List<BaseItemKind> list, BaseItemKind value) + => list.Count == 0 || list.Contains(value); + private static void AddIfMissing(List<BaseItemKind> list, BaseItemKind value) { if (!list.Contains(value)) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs index f557e3732a..7c64d9854d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs @@ -167,43 +167,7 @@ public sealed partial class BaseItemRepository // Build the master query and collapse rows that share a PresentationUniqueKey // (e.g. alternate versions) by picking the lowest Id per group. var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter); - - IQueryable<Guid> orderedMasterQuery; - if (!string.IsNullOrEmpty(filter.SearchTerm)) - { - var cleanSearchTerm = filter.SearchTerm.GetCleanValue(); - var cleanSearchPrefix = cleanSearchTerm + " "; - - orderedMasterQuery = masterQuery - .Select(e => new - { - e.Id, - e.PresentationUniqueKey, - e.SortName, - Score = (e.CleanName == cleanSearchTerm) ? 0 - : e.CleanName!.StartsWith(cleanSearchTerm) ? 1 - : e.CleanName!.Contains(cleanSearchPrefix) ? 2 - : 3 - }) - .GroupBy(x => x.PresentationUniqueKey) - .Select(g => new - { - Id = g.Min(x => x.Id), - Score = g.Min(x => x.Score), - SortName = g.Min(x => x.SortName) - }) - .OrderBy(x => x.Score) - .ThenBy(x => x.SortName) - .Select(x => x.Id); - } - else - { - orderedMasterQuery = masterQuery - .GroupBy(e => e.PresentationUniqueKey) - .Select(g => new { Id = g.Min(e => e.Id), SortName = g.Min(e => e.SortName) }) - .OrderBy(x => x.SortName) - .Select(x => x.Id); - } + var orderedMasterQuery = BuildOrderedMasterQuery(masterQuery, filter.SearchTerm); var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); if (filter.EnableTotalRecordCount) @@ -229,60 +193,10 @@ public sealed partial class BaseItemRepository 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 @@ -300,7 +214,6 @@ public sealed partial class BaseItemRepository } else { - result.StartIndex = filter.StartIndex ?? 0; result.Items = [ .. query @@ -314,4 +227,98 @@ public sealed partial class BaseItemRepository return result; } + + private static IQueryable<Guid> BuildOrderedMasterQuery(IQueryable<BaseItemEntity> masterQuery, string? searchTerm) + { + if (string.IsNullOrEmpty(searchTerm)) + { + return masterQuery + .GroupBy(e => e.PresentationUniqueKey) + .Select(g => new { Id = g.Min(e => e.Id), SortName = g.Min(e => e.SortName) }) + .OrderBy(x => x.SortName) + .Select(x => x.Id); + } + + var cleanSearchTerm = searchTerm.GetCleanValue(); + var cleanSearchPrefix = cleanSearchTerm + " "; + + return masterQuery + .Select(e => new + { + e.Id, + e.PresentationUniqueKey, + e.SortName, + Score = (e.CleanName == cleanSearchTerm) ? 0 + : e.CleanName!.StartsWith(cleanSearchTerm) ? 1 + : e.CleanName!.Contains(cleanSearchPrefix) ? 2 + : 3 + }) + .GroupBy(x => x.PresentationUniqueKey) + .Select(g => new + { + Id = g.Min(x => x.Id), + Score = g.Min(x => x.Score), + SortName = g.Min(x => x.SortName) + }) + .OrderBy(x => x.Score) + .ThenBy(x => x.SortName) + .Select(x => x.Id); + } + + 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), + }); + } } |
