Merge pull request #17724 from Shadowghost/fix-by-name-item-counts
Fix item counts on the by-name endpoints
This commit is contained in:
@@ -126,6 +126,12 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
// Asking for a type filter has always implied wanting that type's counts back.
|
||||
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
|
||||
}
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
@@ -193,31 +199,7 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
|
||||
var result = _libraryManager.GetArtists(query);
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (includeItemTypes.Length != 0)
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
dto.SeriesCount = itemCounts.SeriesCount;
|
||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||
dto.MovieCount = itemCounts.MovieCount;
|
||||
dto.TrailerCount = itemCounts.TrailerCount;
|
||||
dto.AlbumCount = itemCounts.AlbumCount;
|
||||
dto.SongCount = itemCounts.SongCount;
|
||||
dto.ArtistCount = itemCounts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>(
|
||||
query.StartIndex,
|
||||
result.TotalRecordCount,
|
||||
dtos.ToArray());
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -298,6 +280,12 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
// Asking for a type filter has always implied wanting that type's counts back.
|
||||
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
|
||||
}
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
@@ -365,31 +353,7 @@ public class ArtistsController : BaseJellyfinApiController
|
||||
|
||||
var result = _libraryManager.GetAlbumArtists(query);
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (includeItemTypes.Length != 0)
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
dto.SeriesCount = itemCounts.SeriesCount;
|
||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||
dto.MovieCount = itemCounts.MovieCount;
|
||||
dto.TrailerCount = itemCounts.TrailerCount;
|
||||
dto.AlbumCount = itemCounts.AlbumCount;
|
||||
dto.SongCount = itemCounts.SongCount;
|
||||
dto.ArtistCount = itemCounts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>(
|
||||
query.StartIndex,
|
||||
result.TotalRecordCount,
|
||||
dtos.ToArray());
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -97,6 +97,12 @@ public class GenresController : BaseJellyfinApiController
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
// Asking for a type filter has always implied wanting that type's counts back.
|
||||
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
|
||||
}
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
: _userManager.GetUserById(userId.Value);
|
||||
@@ -143,8 +149,7 @@ public class GenresController : BaseJellyfinApiController
|
||||
result = _libraryManager.GetGenres(query);
|
||||
}
|
||||
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -98,6 +98,12 @@ public class MusicGenresController : BaseJellyfinApiController
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
// Asking for a type filter has always implied wanting that type's counts back.
|
||||
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
|
||||
}
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
: _userManager.GetUserById(userId.Value);
|
||||
@@ -134,8 +140,7 @@ public class MusicGenresController : BaseJellyfinApiController
|
||||
|
||||
var result = _libraryManager.GetMusicGenres(query);
|
||||
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -92,6 +92,12 @@ public class StudiosController : BaseJellyfinApiController
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
// Asking for a type filter has always implied wanting that type's counts back.
|
||||
if (includeItemTypes.Length != 0 && !dtoOptions.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.ItemCounts];
|
||||
}
|
||||
|
||||
User? user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
: _userManager.GetUserById(userId.Value);
|
||||
@@ -126,8 +132,7 @@ public class StudiosController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetStudios(query);
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -156,7 +156,6 @@ public static class RequestHelpers
|
||||
QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result,
|
||||
DtoOptions dtoOptions,
|
||||
IDtoService dtoService,
|
||||
bool includeItemTypes,
|
||||
User? user)
|
||||
{
|
||||
var dtos = result.Items.Select(i =>
|
||||
@@ -164,7 +163,7 @@ public static class RequestHelpers
|
||||
var (baseItem, counts) = i;
|
||||
var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (includeItemTypes)
|
||||
if (counts is not null)
|
||||
{
|
||||
dto.ChildCount = counts.ItemCount;
|
||||
dto.ProgramCount = counts.ProgramCount;
|
||||
@@ -175,6 +174,7 @@ public static class RequestHelpers
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.SongCount = counts.SongCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
dto.MusicVideoCount = counts.MusicVideoCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
|
||||
@@ -246,14 +246,20 @@ public sealed partial class BaseItemRepository
|
||||
}
|
||||
|
||||
result.StartIndex = filter.StartIndex ?? 0;
|
||||
if (filter.IncludeItemTypes.Length > 0)
|
||||
var page = query.AsEnumerable().Where(e => e is not null).ToList();
|
||||
|
||||
if (filter.DtoOptions.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes);
|
||||
var pageCleanNames = page
|
||||
.Where(e => !string.IsNullOrEmpty(e.CleanName))
|
||||
.Select(e => e.CleanName!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var countsByCleanName = BuildItemCountsByCleanName(context, filter, itemValueTypes, pageCleanNames);
|
||||
result.Items =
|
||||
[
|
||||
.. query
|
||||
.AsEnumerable()
|
||||
.Where(e => e is not null)
|
||||
.. page
|
||||
.Select(e =>
|
||||
{
|
||||
var item = DeserializeBaseItem(e, filter.SkipDeserialization);
|
||||
@@ -268,9 +274,7 @@ public sealed partial class BaseItemRepository
|
||||
{
|
||||
result.Items =
|
||||
[
|
||||
.. query
|
||||
.AsEnumerable()
|
||||
.Where(e => e != null)
|
||||
.. page
|
||||
.Select(e => DeserializeBaseItem(e, filter.SkipDeserialization))
|
||||
.Where(item => item != null)
|
||||
.Select(item => (item!, (ItemCounts?)null))
|
||||
@@ -281,14 +285,22 @@ public sealed partial class BaseItemRepository
|
||||
}
|
||||
|
||||
private Dictionary<string, ItemCounts> BuildItemCountsByCleanName(
|
||||
Database.Implementations.JellyfinDbContext context,
|
||||
JellyfinDbContext context,
|
||||
InternalItemsQuery filter,
|
||||
IReadOnlyList<ItemValueType> itemValueTypes)
|
||||
IReadOnlyList<ItemValueType> itemValueTypes,
|
||||
IReadOnlyList<string> cleanNames)
|
||||
{
|
||||
var typeSubQuery = new InternalItemsQuery(filter.User)
|
||||
var countsByCleanName = new Dictionary<string, ItemCounts>();
|
||||
if (cleanNames.Count == 0)
|
||||
{
|
||||
return countsByCleanName;
|
||||
}
|
||||
|
||||
// The counts describe everything the value is attached to, not only the types the list was
|
||||
// filtered down to.
|
||||
var scopeQuery = new InternalItemsQuery(filter.User)
|
||||
{
|
||||
ExcludeItemTypes = filter.ExcludeItemTypes,
|
||||
IncludeItemTypes = filter.IncludeItemTypes,
|
||||
MediaTypes = filter.MediaTypes,
|
||||
AncestorIds = filter.AncestorIds,
|
||||
ExcludeItemIds = filter.ExcludeItemIds,
|
||||
@@ -298,33 +310,51 @@ public sealed partial class BaseItemRepository
|
||||
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 scopedItems = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, scopeQuery);
|
||||
var valueLinks = context.ItemValuesMap
|
||||
.AsNoTracking()
|
||||
.Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
|
||||
.WhereOneOrMany(cleanNames, ivm => ivm.ItemValue.CleanValue);
|
||||
|
||||
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 musicVideoTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicVideo];
|
||||
var programTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.LiveTvProgram];
|
||||
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 rawCounts = context.ItemValuesMap
|
||||
.Where(ivm => itemValueTypes.Contains(ivm.ItemValue.Type))
|
||||
.Where(ivm => itemIds.Contains(ivm.ItemId))
|
||||
var rawCounts = valueLinks
|
||||
.Join(
|
||||
context.BaseItems,
|
||||
scopedItems,
|
||||
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() })
|
||||
.AsEnumerable();
|
||||
(ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, e.Type, e.SeriesId })
|
||||
.GroupBy(x => new { x.CleanName, x.Type, x.SeriesId })
|
||||
.Select(g => new { g.Key.CleanName, g.Key.Type, g.Key.SeriesId, Count = g.Count() })
|
||||
.ToList();
|
||||
|
||||
// Only studios and genres pass down from a series to its episodes; an artist credit does not.
|
||||
var inheritsToEpisodes = itemValueTypes.Contains(ItemValueType.Studios) || itemValueTypes.Contains(ItemValueType.Genre);
|
||||
var episodeCounts = inheritsToEpisodes
|
||||
? BuildEpisodeCountsByCleanName(
|
||||
scopedItems,
|
||||
valueLinks,
|
||||
rawCounts
|
||||
.Where(x => x.Type == episodeTypeName)
|
||||
.Select(x => (x.CleanName, x.SeriesId, x.Count))
|
||||
.ToList(),
|
||||
seriesTypeName,
|
||||
episodeTypeName)
|
||||
: rawCounts
|
||||
.Where(x => x.Type == episodeTypeName)
|
||||
.GroupBy(x => x.CleanName)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(x => x.Count));
|
||||
|
||||
var countsByCleanName = new Dictionary<string, ItemCounts>();
|
||||
foreach (var group in rawCounts.GroupBy(x => x.CleanName))
|
||||
{
|
||||
var counts = new ItemCounts();
|
||||
@@ -334,10 +364,6 @@ public sealed partial class BaseItemRepository
|
||||
{
|
||||
counts.SeriesCount += row.Count;
|
||||
}
|
||||
else if (row.Type == episodeTypeName)
|
||||
{
|
||||
counts.EpisodeCount += row.Count;
|
||||
}
|
||||
else if (row.Type == movieTypeName)
|
||||
{
|
||||
counts.MovieCount += row.Count;
|
||||
@@ -350,6 +376,14 @@ public sealed partial class BaseItemRepository
|
||||
{
|
||||
counts.ArtistCount += row.Count;
|
||||
}
|
||||
else if (row.Type == musicVideoTypeName)
|
||||
{
|
||||
counts.MusicVideoCount += row.Count;
|
||||
}
|
||||
else if (row.Type == programTypeName)
|
||||
{
|
||||
counts.ProgramCount += row.Count;
|
||||
}
|
||||
else if (row.Type == audioTypeName)
|
||||
{
|
||||
counts.SongCount += row.Count;
|
||||
@@ -360,9 +394,72 @@ public sealed partial class BaseItemRepository
|
||||
}
|
||||
}
|
||||
|
||||
// Episodes are counted separately: the value is usually only written on the series.
|
||||
counts.EpisodeCount = episodeCounts.GetValueOrDefault(group.Key);
|
||||
counts.ItemCount = counts.TotalItemCount();
|
||||
countsByCleanName[group.Key] = counts;
|
||||
}
|
||||
|
||||
// A value carried by nothing but the episodes below a tagged series has no row of its own.
|
||||
foreach (var (cleanName, episodeCount) in episodeCounts)
|
||||
{
|
||||
if (!countsByCleanName.ContainsKey(cleanName))
|
||||
{
|
||||
countsByCleanName[cleanName] = new ItemCounts { EpisodeCount = episodeCount, ItemCount = episodeCount };
|
||||
}
|
||||
}
|
||||
|
||||
return countsByCleanName;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> BuildEpisodeCountsByCleanName(
|
||||
IQueryable<BaseItemEntity> scopedItems,
|
||||
IQueryable<ItemValueMap> valueLinks,
|
||||
IReadOnlyList<(string CleanName, Guid? SeriesId, int Count)> taggedEpisodes,
|
||||
string seriesTypeName,
|
||||
string episodeTypeName)
|
||||
{
|
||||
// Resolved in steps rather than as one union: each of these drives off an index, while the
|
||||
// single-statement form leaves SQLite free to scan every episode in the library instead.
|
||||
var taggedSeries = valueLinks
|
||||
.Join(
|
||||
scopedItems.Where(e => e.Type == seriesTypeName),
|
||||
ivm => ivm.ItemId,
|
||||
e => e.Id,
|
||||
(ivm, e) => new { CleanName = ivm.ItemValue.CleanValue, SeriesId = e.Id })
|
||||
.ToList();
|
||||
|
||||
var seriesIds = taggedSeries.Select(x => x.SeriesId).Distinct().ToArray();
|
||||
var episodesPerSeries = seriesIds.Length == 0
|
||||
? []
|
||||
: scopedItems
|
||||
.Where(e => e.Type == episodeTypeName && e.SeriesId != null)
|
||||
.WhereOneOrMany(seriesIds, e => e.SeriesId!.Value)
|
||||
.GroupBy(e => e.SeriesId!.Value)
|
||||
.Select(g => new { SeriesId = g.Key, Count = g.Count() })
|
||||
.ToDictionary(x => x.SeriesId, x => x.Count);
|
||||
|
||||
var episodeCounts = new Dictionary<string, int>();
|
||||
var seriesByCleanName = new Dictionary<string, HashSet<Guid>>();
|
||||
foreach (var group in taggedSeries.GroupBy(x => x.CleanName))
|
||||
{
|
||||
var series = group.Select(x => x.SeriesId).ToHashSet();
|
||||
seriesByCleanName[group.Key] = series;
|
||||
episodeCounts[group.Key] = series.Sum(id => episodesPerSeries.GetValueOrDefault(id));
|
||||
}
|
||||
|
||||
foreach (var (cleanName, seriesId, count) in taggedEpisodes)
|
||||
{
|
||||
if (seriesId is not null
|
||||
&& seriesByCleanName.TryGetValue(cleanName, out var series)
|
||||
&& series.Contains(seriesId.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
episodeCounts[cleanName] = episodeCounts.GetValueOrDefault(cleanName) + count;
|
||||
}
|
||||
|
||||
return episodeCounts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,11 +249,51 @@ public class ItemCountService : IItemCountService
|
||||
}
|
||||
}
|
||||
|
||||
if (kind is BaseItemKind.Studio or BaseItemKind.Genre or BaseItemKind.MusicGenre
|
||||
&& relatedItemKinds.Contains(BaseItemKind.Episode)
|
||||
&& relatedItemKinds.Contains(BaseItemKind.Series))
|
||||
{
|
||||
var rolledUpEpisodeCount = CountEpisodesOfTaggedSeries(context, baseQuery, accessFilter, out var directEpisodeCount);
|
||||
totalCount += rolledUpEpisodeCount - result.EpisodeCount + directEpisodeCount;
|
||||
result.EpisodeCount = rolledUpEpisodeCount + directEpisodeCount;
|
||||
}
|
||||
|
||||
result.ItemCount = totalCount;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private int CountEpisodesOfTaggedSeries(
|
||||
JellyfinDbContext context,
|
||||
IQueryable<BaseItemEntity> taggedItems,
|
||||
InternalItemsQuery accessFilter,
|
||||
out int unrelatedEpisodeCount)
|
||||
{
|
||||
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
|
||||
var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode];
|
||||
|
||||
var taggedSeriesIds = taggedItems.Where(e => e.Type == seriesTypeName).Select(e => e.Id);
|
||||
unrelatedEpisodeCount = taggedItems.Count(e => e.Type == episodeTypeName
|
||||
&& (e.SeriesId == null || !taggedSeriesIds.Contains(e.SeriesId.Value)));
|
||||
|
||||
// Materialised so the episode count drives off IX_BaseItems_SeriesId.
|
||||
var seriesIds = taggedItems
|
||||
.Where(e => e.Type == seriesTypeName)
|
||||
.Select(e => e.Id)
|
||||
.ToArray();
|
||||
|
||||
if (seriesIds.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var episodes = context.BaseItems.AsNoTracking()
|
||||
.Where(e => e.Type == episodeTypeName && e.SeriesId != null)
|
||||
.WhereOneOrMany(seriesIds, e => e.SeriesId!.Value);
|
||||
|
||||
return _queryHelpers.ApplyAccessFiltering(context, episodes, accessFilter).Count();
|
||||
}
|
||||
|
||||
private static IQueryable<BaseItemEntity> ItemsById(JellyfinDbContext context, IQueryable<Guid> itemIds)
|
||||
=> context.BaseItems.AsNoTracking().Where(e => itemIds.Contains(e.Id));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user