mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-06-03 05:34:16 -04:00
Fix ItemValue query (#13939)
This commit is contained in:
parent
f576783ae1
commit
1c4b5199b8
@ -148,37 +148,37 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter)
|
public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
|
return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter)
|
public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
|
return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
|
public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
|
return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter)
|
public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
|
return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter)
|
public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
|
return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
|
public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
|
return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]);
|
||||||
}
|
}
|
||||||
@ -402,7 +402,8 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
|
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
|
||||||
{
|
{
|
||||||
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking().AsSplitQuery()
|
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
|
||||||
|
dbQuery = dbQuery.AsSingleQuery()
|
||||||
.Include(e => e.TrailerTypes)
|
.Include(e => e.TrailerTypes)
|
||||||
.Include(e => e.Provider)
|
.Include(e => e.Provider)
|
||||||
.Include(e => e.LockedFields);
|
.Include(e => e.LockedFields);
|
||||||
@ -1037,7 +1038,7 @@ public sealed class BaseItemRepository
|
|||||||
return Map(baseItemEntity, dto, appHost);
|
return Map(baseItemEntity, dto, appHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
|
private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList<ItemValueType> itemValueTypes, string returnType)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(filter);
|
ArgumentNullException.ThrowIfNull(filter);
|
||||||
|
|
||||||
@ -1048,20 +1049,59 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
using var context = _dbProvider.CreateDbContext();
|
using var context = _dbProvider.CreateDbContext();
|
||||||
|
|
||||||
var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
|
var innerQueryFilter = TranslateQuery(context.BaseItems, context, new InternalItemsQuery(filter.User)
|
||||||
|
|
||||||
query = query.Where(e => e.Type == returnType);
|
|
||||||
// this does not seem to be nesseary but it does not make any sense why this isn't working.
|
|
||||||
// && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
|
|
||||||
|
|
||||||
if (filter.OrderBy.Count != 0
|
|
||||||
|| !string.IsNullOrEmpty(filter.SearchTerm))
|
|
||||||
{
|
{
|
||||||
query = ApplyOrder(query, filter);
|
ExcludeItemTypes = filter.ExcludeItemTypes,
|
||||||
}
|
IncludeItemTypes = filter.IncludeItemTypes,
|
||||||
else
|
MediaTypes = filter.MediaTypes,
|
||||||
|
AncestorIds = filter.AncestorIds,
|
||||||
|
ItemIds = filter.ItemIds,
|
||||||
|
TopParentIds = filter.TopParentIds,
|
||||||
|
ParentId = filter.ParentId,
|
||||||
|
IsAiring = filter.IsAiring,
|
||||||
|
IsMovie = filter.IsMovie,
|
||||||
|
IsSports = filter.IsSports,
|
||||||
|
IsKids = filter.IsKids,
|
||||||
|
IsNews = filter.IsNews,
|
||||||
|
IsSeries = filter.IsSeries
|
||||||
|
});
|
||||||
|
|
||||||
|
var innerQuery = PrepareItemQuery(context, filter)
|
||||||
|
.Where(e => e.Type == returnType)
|
||||||
|
.Where(e => context.ItemValues!
|
||||||
|
.Where(f => itemValueTypes.Contains(f.Type))
|
||||||
|
.Where(f => innerQueryFilter.Any(g => f.BaseItemsMap!.Any(w => w.ItemId == g.Id)))
|
||||||
|
.Select(f => f.CleanValue)
|
||||||
|
.Contains(e.CleanName));
|
||||||
|
|
||||||
|
var outerQueryFilter = new InternalItemsQuery(filter.User)
|
||||||
{
|
{
|
||||||
query = query.OrderBy(e => e.SortName);
|
IsPlayed = filter.IsPlayed,
|
||||||
|
IsFavorite = filter.IsFavorite,
|
||||||
|
IsFavoriteOrLiked = filter.IsFavoriteOrLiked,
|
||||||
|
IsLiked = filter.IsLiked,
|
||||||
|
IsLocked = filter.IsLocked,
|
||||||
|
NameLessThan = filter.NameLessThan,
|
||||||
|
NameStartsWith = filter.NameStartsWith,
|
||||||
|
NameStartsWithOrGreater = filter.NameStartsWithOrGreater,
|
||||||
|
Tags = filter.Tags,
|
||||||
|
OfficialRatings = filter.OfficialRatings,
|
||||||
|
StudioIds = filter.StudioIds,
|
||||||
|
GenreIds = filter.GenreIds,
|
||||||
|
Genres = filter.Genres,
|
||||||
|
Years = filter.Years,
|
||||||
|
NameContains = filter.NameContains,
|
||||||
|
SearchTerm = filter.SearchTerm,
|
||||||
|
ExcludeItemIds = filter.ExcludeItemIds
|
||||||
|
};
|
||||||
|
|
||||||
|
var query = TranslateQuery(innerQuery, context, outerQueryFilter)
|
||||||
|
.GroupBy(e => e.PresentationUniqueKey);
|
||||||
|
|
||||||
|
var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
|
||||||
|
if (filter.EnableTotalRecordCount)
|
||||||
|
{
|
||||||
|
result.TotalRecordCount = query.Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
|
||||||
@ -1079,41 +1119,84 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new QueryResult<(BaseItemDto, ItemCounts)>();
|
IQueryable<BaseItemEntity>? itemCountQuery = null;
|
||||||
if (filter.EnableTotalRecordCount)
|
|
||||||
{
|
|
||||||
result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count();
|
|
||||||
}
|
|
||||||
|
|
||||||
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
|
if (filter.IncludeItemTypes.Length > 0)
|
||||||
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 resultQuery = query.Select(e => new
|
|
||||||
{
|
{
|
||||||
item = e,
|
// if we are to include more then one type, sub query those items beforehand.
|
||||||
// TODO: This is bad refactor!
|
|
||||||
itemCount = new ItemCounts()
|
var typeSubQuery = new InternalItemsQuery(filter.User)
|
||||||
{
|
{
|
||||||
SeriesCount = e.ItemValues!.Count(f => f.Item.Type == seriesTypeName),
|
ExcludeItemTypes = filter.ExcludeItemTypes,
|
||||||
EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == episodeTypeName),
|
IncludeItemTypes = filter.IncludeItemTypes,
|
||||||
MovieCount = e.ItemValues!.Count(f => f.Item.Type == movieTypeName),
|
MediaTypes = filter.MediaTypes,
|
||||||
AlbumCount = e.ItemValues!.Count(f => f.Item.Type == musicAlbumTypeName),
|
AncestorIds = filter.AncestorIds,
|
||||||
ArtistCount = e.ItemValues!.Count(f => f.Item.Type == musicArtistTypeName),
|
ExcludeItemIds = filter.ExcludeItemIds,
|
||||||
SongCount = e.ItemValues!.Count(f => f.Item.Type == audioTypeName),
|
ItemIds = filter.ItemIds,
|
||||||
TrailerCount = e.ItemValues!.Count(f => f.Item.Type == trailerTypeName),
|
TopParentIds = filter.TopParentIds,
|
||||||
}
|
ParentId = filter.ParentId,
|
||||||
});
|
IsPlayed = filter.IsPlayed
|
||||||
|
};
|
||||||
|
|
||||||
result.StartIndex = filter.StartIndex ?? 0;
|
itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, typeSubQuery)
|
||||||
result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e =>
|
.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 resultQuery = query.Select(e => new
|
||||||
|
{
|
||||||
|
item = e.AsQueryable()
|
||||||
|
.Include(e => e.TrailerTypes)
|
||||||
|
.Include(e => e.Provider)
|
||||||
|
.Include(e => e.LockedFields)
|
||||||
|
.Include(e => e.Images)
|
||||||
|
.AsSingleQuery().First(),
|
||||||
|
// TODO: This is bad refactor!
|
||||||
|
itemCount = new ItemCounts()
|
||||||
|
{
|
||||||
|
SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName),
|
||||||
|
EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName),
|
||||||
|
MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName),
|
||||||
|
AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName),
|
||||||
|
ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName),
|
||||||
|
SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName),
|
||||||
|
TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.StartIndex = filter.StartIndex ?? 0;
|
||||||
|
result.Items =
|
||||||
|
[
|
||||||
|
.. resultQuery
|
||||||
|
.AsEnumerable()
|
||||||
|
.Where(e => e is not null)
|
||||||
|
.Select(e =>
|
||||||
|
{
|
||||||
|
return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
result.StartIndex = filter.StartIndex ?? 0;
|
||||||
}).ToArray();
|
result.Items =
|
||||||
|
[
|
||||||
|
.. query
|
||||||
|
.Select(e => e.First())
|
||||||
|
.AsEnumerable()
|
||||||
|
.Where(e => e is not null)
|
||||||
|
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||||
|
{
|
||||||
|
return (DeserialiseBaseItem(e, filter.SkipDeserialization), null);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -1296,7 +1379,7 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
else if (orderBy.Count == 0)
|
else if (orderBy.Count == 0)
|
||||||
{
|
{
|
||||||
return query;
|
return query.OrderBy(e => e.SortName);
|
||||||
}
|
}
|
||||||
|
|
||||||
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
|
||||||
@ -1478,6 +1561,7 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
var includeTypes = filter.IncludeItemTypes;
|
var includeTypes = filter.IncludeItemTypes;
|
||||||
|
|
||||||
// Only specify excluded types if no included types are specified
|
// Only specify excluded types if no included types are specified
|
||||||
if (filter.IncludeItemTypes.Length == 0)
|
if (filter.IncludeItemTypes.Length == 0)
|
||||||
{
|
{
|
||||||
@ -1503,25 +1587,10 @@ public sealed class BaseItemRepository
|
|||||||
baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
|
baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (includeTypes.Length == 1)
|
else
|
||||||
{
|
{
|
||||||
if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
|
string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e => e != null).ToArray()!;
|
||||||
{
|
baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
|
||||||
baseQuery = baseQuery.Where(e => e.Type == includeTypeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (includeTypes.Length > 1)
|
|
||||||
{
|
|
||||||
var includeTypeName = new List<string>();
|
|
||||||
foreach (var includeType in includeTypes)
|
|
||||||
{
|
|
||||||
if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
|
|
||||||
{
|
|
||||||
includeTypeName.Add(baseItemKindName!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.ChannelIds.Count > 0)
|
if (filter.ChannelIds.Count > 0)
|
||||||
@ -1779,64 +1848,59 @@ public sealed class BaseItemRepository
|
|||||||
|
|
||||||
if (filter.ArtistIds.Length > 0)
|
if (filter.ArtistIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds);
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.AlbumArtistIds.Length > 0)
|
if (filter.AlbumArtistIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.AlbumArtistIds);
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.ContributingArtistIds.Length > 0)
|
if (filter.ContributingArtistIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.AlbumIds.Length > 0)
|
if (filter.AlbumIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery.Where(e => context.BaseItems.Where(f => filter.AlbumIds.Contains(f.Id)).Any(f => f.Name == e.Album));
|
var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id);
|
||||||
|
baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.ExcludeArtistIds.Length > 0)
|
if (filter.ExcludeArtistIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true);
|
||||||
.Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.GenreIds.Count > 0)
|
if (filter.GenreIds.Count > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Genres.Count > 0)
|
if (filter.Genres.Count > 0)
|
||||||
{
|
{
|
||||||
var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray();
|
var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && cleanGenres.Contains(f.ItemValue.CleanValue)));
|
.Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(cleanGenres));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.Count > 0)
|
if (tags.Count > 0)
|
||||||
{
|
{
|
||||||
var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray();
|
var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
|
.Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (excludeTags.Count > 0)
|
if (excludeTags.Count > 0)
|
||||||
{
|
{
|
||||||
var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray();
|
var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder<ItemValueMap, string>(f => f.ItemValue.CleanValue);
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery
|
||||||
.Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue)));
|
.Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.StudioIds.Length > 0)
|
if (filter.StudioIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
|
||||||
.Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.OfficialRatings.Length > 0)
|
if (filter.OfficialRatings.Length > 0)
|
||||||
@ -2027,15 +2091,9 @@ public sealed class BaseItemRepository
|
|||||||
.Where(e => !context.Peoples.Any(f => f.Name == e.Name));
|
.Where(e => !context.Peoples.Any(f => f.Name == e.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.Years.Length == 1)
|
if (filter.Years.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
|
||||||
.Where(e => e.ProductionYear == filter.Years[0]);
|
|
||||||
}
|
|
||||||
else if (filter.Years.Length > 1)
|
|
||||||
{
|
|
||||||
baseQuery = baseQuery
|
|
||||||
.Where(e => filter.Years.Any(f => f == e.ProductionYear));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
|
var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
|
||||||
@ -2076,14 +2134,12 @@ public sealed class BaseItemRepository
|
|||||||
if (filter.MediaTypes.Length > 0)
|
if (filter.MediaTypes.Length > 0)
|
||||||
{
|
{
|
||||||
var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
|
var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
|
||||||
.Where(e => mediaTypes.Contains(e.MediaType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.ItemIds.Length > 0)
|
if (filter.ItemIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery
|
baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
|
||||||
.Where(e => filter.ItemIds.Contains(e.Id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.ExcludeItemIds.Length > 0)
|
if (filter.ExcludeItemIds.Length > 0)
|
||||||
@ -2129,13 +2185,13 @@ public sealed class BaseItemRepository
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery.Where(e => queryTopParentIds.Contains(e.TopParentId!.Value));
|
baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.AncestorIds.Length > 0)
|
if (filter.AncestorIds.Length > 0)
|
||||||
{
|
{
|
||||||
baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
|
baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ItemId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
|
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
|
||||||
|
@ -0,0 +1,166 @@
|
|||||||
|
#pragma warning disable RS0030 // Do not use banned APIs
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Jellyfin.Database.Implementations.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jellyfin.Database.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a number of query related extensions.
|
||||||
|
/// </summary>
|
||||||
|
public static class JellyfinQueryHelperExtensions
|
||||||
|
{
|
||||||
|
private static readonly MethodInfo _containsMethodGenericCache = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static).First(m => m.Name == nameof(Enumerable.Contains) && m.GetParameters().Length == 2);
|
||||||
|
private static readonly MethodInfo _efParameterInstruction = typeof(EF).GetMethod(nameof(EF.Parameter), BindingFlags.Public | BindingFlags.Static)!;
|
||||||
|
private static readonly ConcurrentDictionary<Type, MethodInfo> _containsQueryCache = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an optimised query checking one property against a list of values while maintaining an optimal query.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity">The entity.</typeparam>
|
||||||
|
/// <typeparam name="TProperty">The property type to compare.</typeparam>
|
||||||
|
/// <param name="query">The source query.</param>
|
||||||
|
/// <param name="oneOf">The list of items to check.</param>
|
||||||
|
/// <param name="property">Property expression.</param>
|
||||||
|
/// <returns>A Query.</returns>
|
||||||
|
public static IQueryable<TEntity> WhereOneOrMany<TEntity, TProperty>(this IQueryable<TEntity> query, IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
|
||||||
|
{
|
||||||
|
return query.Where(OneOrManyExpressionBuilder(oneOf, property));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseQuery">The source query.</param>
|
||||||
|
/// <param name="context">The database context.</param>
|
||||||
|
/// <param name="itemValueType">The type of item value to reference.</param>
|
||||||
|
/// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
|
||||||
|
/// <param name="invert">If set an exclusion check is performed instead.</param>
|
||||||
|
/// <returns>A Query.</returns>
|
||||||
|
public static IQueryable<BaseItemEntity> WhereReferencedItem(
|
||||||
|
this IQueryable<BaseItemEntity> baseQuery,
|
||||||
|
JellyfinDbContext context,
|
||||||
|
ItemValueType itemValueType,
|
||||||
|
IList<Guid> referenceIds,
|
||||||
|
bool invert = false)
|
||||||
|
{
|
||||||
|
return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The database context.</param>
|
||||||
|
/// <param name="itemValueType">The type of item value to reference.</param>
|
||||||
|
/// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
|
||||||
|
/// <param name="invert">If set an exclusion check is performed instead.</param>
|
||||||
|
/// <returns>A Query.</returns>
|
||||||
|
public static Expression<Func<BaseItemEntity, bool>> ReferencedItemFilterExpressionBuilder(
|
||||||
|
this JellyfinDbContext context,
|
||||||
|
ItemValueType itemValueType,
|
||||||
|
IList<Guid> referenceIds,
|
||||||
|
bool invert = false)
|
||||||
|
{
|
||||||
|
// Well genre/artist/album etc items do not actually set the ItemValue of thier specitic types so we cannot match it that way.
|
||||||
|
/*
|
||||||
|
"(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreIds and Type=2)))"
|
||||||
|
*/
|
||||||
|
|
||||||
|
var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
|
||||||
|
|
||||||
|
return item =>
|
||||||
|
context.ItemValues
|
||||||
|
.Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (item, map) => new { item, map })
|
||||||
|
.Any(val =>
|
||||||
|
val.item.Type == itemValueType
|
||||||
|
&& context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.item.CleanValue)
|
||||||
|
&& val.map.ItemId == item.Id) == EF.Constant(!invert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an optimised query expression checking one property against a list of values while maintaining an optimal query.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEntity">The entity.</typeparam>
|
||||||
|
/// <typeparam name="TProperty">The property type to compare.</typeparam>
|
||||||
|
/// <param name="oneOf">The list of items to check.</param>
|
||||||
|
/// <param name="property">Property expression.</param>
|
||||||
|
/// <returns>A Query.</returns>
|
||||||
|
public static Expression<Func<TEntity, bool>> OneOrManyExpressionBuilder<TEntity, TProperty>(this IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
|
||||||
|
{
|
||||||
|
var parameter = Expression.Parameter(typeof(TEntity), "item");
|
||||||
|
property = ParameterReplacer.Replace<Func<TEntity, TProperty>, Func<TEntity, TProperty>>(property, property.Parameters[0], parameter);
|
||||||
|
if (oneOf.Count == 1)
|
||||||
|
{
|
||||||
|
var value = oneOf[0];
|
||||||
|
if (typeof(TProperty).IsValueType)
|
||||||
|
{
|
||||||
|
return Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(property.Body, Expression.Constant(value)), parameter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Expression.Lambda<Func<TEntity, bool>>(Expression.ReferenceEqual(property.Body, Expression.Constant(value)), parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key));
|
||||||
|
|
||||||
|
if (oneOf.Count < 4) // arbitrary value choosen.
|
||||||
|
{
|
||||||
|
// if we have 3 or fewer values to check against its faster to do a IN(const,const,const) lookup
|
||||||
|
return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Constant(oneOf), property.Body), parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), property.Body), parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ParameterReplacer
|
||||||
|
{
|
||||||
|
// Produces an expression identical to 'expression'
|
||||||
|
// except with 'source' parameter replaced with 'target' expression.
|
||||||
|
internal static Expression<TOutput> Replace<TInput, TOutput>(
|
||||||
|
Expression<TInput> expression,
|
||||||
|
ParameterExpression source,
|
||||||
|
ParameterExpression target)
|
||||||
|
{
|
||||||
|
return new ParameterReplacerVisitor<TOutput>(source, target)
|
||||||
|
.VisitAndConvert(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ParameterReplacerVisitor<TOutput> : ExpressionVisitor
|
||||||
|
{
|
||||||
|
private readonly ParameterExpression _source;
|
||||||
|
private readonly ParameterExpression _target;
|
||||||
|
|
||||||
|
public ParameterReplacerVisitor(ParameterExpression source, ParameterExpression target)
|
||||||
|
{
|
||||||
|
_source = source;
|
||||||
|
_target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Expression<TOutput> VisitAndConvert<T>(Expression<T> root)
|
||||||
|
{
|
||||||
|
return (Expression<TOutput>)VisitLambda(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitLambda<T>(Expression<T> node)
|
||||||
|
{
|
||||||
|
// Leave all parameters alone except the one we want to replace.
|
||||||
|
var parameters = node.Parameters.Select(p => p == _source ? _target : p);
|
||||||
|
|
||||||
|
return Expression.Lambda<TOutput>(Visit(node.Body), parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitParameter(ParameterExpression node)
|
||||||
|
{
|
||||||
|
// Replace the source with the target, visit other params as usual.
|
||||||
|
return node == _source ? _target : base.VisitParameter(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user