aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/Search/SearchManager.cs151
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.ByName.cs187
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),
+ });
+ }
}