mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-08 08:48:48 +01:00
Fix Sonar complaints
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user