aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/BaseItemRepository.cs')
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs971
1 files changed, 673 insertions, 298 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 7f4364cf6..2c18ce69a 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -14,6 +14,7 @@ using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
@@ -54,6 +55,11 @@ public sealed class BaseItemRepository
: IItemRepository
{
/// <summary>
+ /// Gets the placeholder id for UserData detached items.
+ /// </summary>
+ public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
+
+ /// <summary>
/// This holds all the types in the running assemblies
/// so that we can de-serialize properly when we don't have strong types.
/// </summary>
@@ -69,6 +75,7 @@ public sealed class BaseItemRepository
private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
+ private static readonly IReadOnlyList<char> SearchWildcardTerms = ['%', '_', '[', ']', '^'];
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
@@ -93,33 +100,59 @@ public sealed class BaseItemRepository
}
/// <inheritdoc />
- public void DeleteItem(Guid id)
+ public void DeleteItem(params IReadOnlyList<Guid> ids)
{
- if (id.IsEmpty())
+ if (ids is null || ids.Count == 0 || ids.Any(f => f.Equals(PlaceholderId)))
{
- throw new ArgumentException("Guid can't be empty", nameof(id));
+ throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(ids));
}
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
- context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
- context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
- context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
- context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete();
- context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete();
- context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete();
- context.BaseItems.Where(e => e.Id == id).ExecuteDelete();
- context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
- context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
- context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete();
+
+ var date = (DateTime?)DateTime.UtcNow;
+
+ var relatedItems = ids.SelectMany(f => TraverseHirachyDown(f, context)).ToArray();
+
+ // Remove any UserData entries for the placeholder item that would conflict with the UserData
+ // being detached from the item being deleted. This is necessary because, during an update,
+ // UserData may be reattached to a new entry, but some entries can be left behind.
+ // Ensures there are no duplicate UserId/CustomDataKey combinations for the placeholder.
+ context.UserData
+ .Join(
+ context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId),
+ placeholder => new { placeholder.UserId, placeholder.CustomDataKey },
+ userData => new { userData.UserId, userData.CustomDataKey },
+ (placeholder, userData) => placeholder)
+ .Where(e => e.ItemId == PlaceholderId)
+ .ExecuteDelete();
+
+ // Detach all user watch data
+ context.UserData.WhereOneOrMany(relatedItems, e => e.ItemId)
+ .ExecuteUpdate(e => e
+ .SetProperty(f => f.RetentionDate, date)
+ .SetProperty(f => f.ItemId, PlaceholderId));
+
+ context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.AncestorIds.WhereOneOrMany(relatedItems, e => e.ParentItemId).ExecuteDelete();
+ context.AttachmentStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.BaseItemImageInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.BaseItemMetadataFields.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.BaseItemProviders.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.BaseItemTrailerTypes.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete();
+ context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete();
- context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
- context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete();
- context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete();
- context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
- context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
- context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
- context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete();
+ context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray();
+ context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
+ context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
+ context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete();
context.SaveChanges();
transaction.Commit();
}
@@ -144,41 +177,41 @@ public sealed class BaseItemRepository
PrepareFilterQuery(filter);
using var context = _dbProvider.CreateDbContext();
- return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
+ return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, filter).Select(e => e.Id).ToArray();
}
/// <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]);
}
/// <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]);
}
/// <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]);
}
/// <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]);
}
/// <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]);
}
/// <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]);
}
@@ -234,15 +267,17 @@ public sealed class BaseItemRepository
IQueryable<BaseItemEntity> dbQuery = PrepareItemQuery(context, filter);
dbQuery = TranslateQuery(dbQuery, context, filter);
+ dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
+
if (filter.EnableTotalRecordCount)
{
result.TotalRecordCount = dbQuery.Count();
}
- dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
+ dbQuery = ApplyNavigations(dbQuery, filter);
- result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+ result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
result.StartIndex = filter.StartIndex ?? 0;
return result;
}
@@ -258,10 +293,11 @@ public sealed class BaseItemRepository
dbQuery = TranslateQuery(dbQuery, context, filter);
- dbQuery = ApplyGroupingFilter(dbQuery, filter);
+ dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
+ dbQuery = ApplyNavigations(dbQuery, filter);
- return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+ return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
}
/// <inheritdoc/>
@@ -300,10 +336,12 @@ public sealed class BaseItemRepository
var mainquery = PrepareItemQuery(context, filter);
mainquery = TranslateQuery(mainquery, context, filter);
mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated));
- mainquery = ApplyGroupingFilter(mainquery, filter);
+ mainquery = ApplyGroupingFilter(context, mainquery, filter);
mainquery = ApplyQueryPaging(mainquery, filter);
- return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray();
+ mainquery = ApplyNavigations(mainquery, filter);
+
+ return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
}
/// <inheritdoc />
@@ -319,7 +357,7 @@ public sealed class BaseItemRepository
.Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
.Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
.Join(
- context.UserData.AsNoTracking(),
+ context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
i => new { UserId = filter.User.Id, ItemId = i.Id },
u => new { UserId = u.UserId, ItemId = u.ItemId },
(entity, data) => new { Item = entity, UserData = data })
@@ -337,36 +375,50 @@ public sealed class BaseItemRepository
return query.ToArray();
}
- private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+ private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
// This whole block is needed to filter duplicate entries on request
// for the time being it cannot be used because it would destroy the ordering
// this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but
// for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own
- // var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
- // if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
- // {
- // dbQuery = ApplyOrder(dbQuery, filter);
- // dbQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.First());
- // }
- // else if (enableGroupByPresentationUniqueKey)
- // {
- // dbQuery = ApplyOrder(dbQuery, filter);
- // dbQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First());
- // }
- // else if (filter.GroupBySeriesPresentationUniqueKey)
- // {
- // dbQuery = ApplyOrder(dbQuery, filter);
- // dbQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.First());
- // }
- // else
- // {
- // dbQuery = dbQuery.Distinct();
- // dbQuery = ApplyOrder(dbQuery, filter);
- // }
- dbQuery = dbQuery.Distinct();
- dbQuery = ApplyOrder(dbQuery, filter);
+ var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
+ if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
+ {
+ var tempQuery = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
+ dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
+ }
+ else if (enableGroupByPresentationUniqueKey)
+ {
+ var tempQuery = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
+ dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
+ }
+ else if (filter.GroupBySeriesPresentationUniqueKey)
+ {
+ var tempQuery = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.FirstOrDefault()).Select(e => e!.Id);
+ dbQuery = context.BaseItems.Where(e => tempQuery.Contains(e.Id));
+ }
+ else
+ {
+ dbQuery = dbQuery.Distinct();
+ }
+
+ dbQuery = ApplyOrder(dbQuery, filter, context);
+
+ return dbQuery;
+ }
+
+ private static IQueryable<BaseItemEntity> ApplyNavigations(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+ {
+ dbQuery = dbQuery.Include(e => e.TrailerTypes)
+ .Include(e => e.Provider)
+ .Include(e => e.LockedFields)
+ .Include(e => e.UserData);
+
+ if (filter.DtoOptions.EnableImages)
+ {
+ dbQuery = dbQuery.Include(e => e.Images);
+ }
return dbQuery;
}
@@ -394,23 +446,16 @@ public sealed class BaseItemRepository
private IQueryable<BaseItemEntity> ApplyQueryFilter(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, InternalItemsQuery filter)
{
dbQuery = TranslateQuery(dbQuery, context, filter);
- dbQuery = ApplyOrder(dbQuery, filter);
- dbQuery = ApplyGroupingFilter(dbQuery, filter);
+ dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
+ dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery;
}
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
{
- IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking().AsSplitQuery()
- .Include(e => e.TrailerTypes)
- .Include(e => e.Provider)
- .Include(e => e.LockedFields);
-
- if (filter.DtoOptions.EnableImages)
- {
- dbQuery = dbQuery.Include(e => e.Images);
- }
+ IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
+ dbQuery = dbQuery.AsSingleQuery();
return dbQuery;
}
@@ -428,6 +473,66 @@ public sealed class BaseItemRepository
return dbQuery.Count();
}
+ /// <inheritdoc />
+ public ItemCounts GetItemCounts(InternalItemsQuery filter)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ // Hack for right now since we currently don't support filtering out these duplicates within a query
+ PrepareFilterQuery(filter);
+
+ using var context = _dbProvider.CreateDbContext();
+ var dbQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
+
+ var counts = dbQuery
+ .GroupBy(x => x.Type)
+ .Select(x => new { x.Key, Count = x.Count() })
+ .ToArray();
+
+ var lookup = _itemTypeLookup.BaseItemKindNames;
+ var result = new ItemCounts();
+ foreach (var count in counts)
+ {
+ if (string.Equals(count.Key, lookup[BaseItemKind.MusicAlbum], StringComparison.Ordinal))
+ {
+ result.AlbumCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicArtist], StringComparison.Ordinal))
+ {
+ result.ArtistCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Episode], StringComparison.Ordinal))
+ {
+ result.EpisodeCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Movie], StringComparison.Ordinal))
+ {
+ result.MovieCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.MusicVideo], StringComparison.Ordinal))
+ {
+ result.MusicVideoCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.LiveTvProgram], StringComparison.Ordinal))
+ {
+ result.ProgramCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Series], StringComparison.Ordinal))
+ {
+ result.SeriesCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Audio], StringComparison.Ordinal))
+ {
+ result.SongCount = count.Count;
+ }
+ else if (string.Equals(count.Key, lookup[BaseItemKind.Trailer], StringComparison.Ordinal))
+ {
+ result.TrailerCount = count.Count;
+ }
+ }
+
+ return result;
+ }
+
#pragma warning disable CA1307 // Specify StringComparison for clarity
/// <summary>
/// Gets the type.
@@ -453,11 +558,16 @@ public sealed class BaseItemRepository
var images = item.ImageInfos.Select(e => Map(item.Id, e));
using var context = _dbProvider.CreateDbContext();
- using var transaction = context.Database.BeginTransaction();
+
+ if (!context.BaseItems.Any(bi => bi.Id == item.Id))
+ {
+ _logger.LogWarning("Unable to save ImageInfo for non existing BaseItem");
+ return;
+ }
+
context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete();
context.BaseItemImageInfos.AddRange(images);
context.SaveChanges();
- transaction.Commit();
}
/// <inheritdoc />
@@ -473,7 +583,7 @@ public sealed class BaseItemRepository
cancellationToken.ThrowIfCancellationRequested();
var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
- foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
+ foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
{
var ancestorIds = item.SupportsAncestors ?
item.GetAncestorIds().Distinct().ToList() :
@@ -487,78 +597,140 @@ public sealed class BaseItemRepository
tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
}
- var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>();
-
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
+
+ var ids = tuples.Select(f => f.Item.Id).ToArray();
+ var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
+ var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
+
foreach (var item in tuples)
{
var entity = Map(item.Item);
// TODO: refactor this "inconsistency"
entity.TopParentId = item.TopParent?.Id;
- if (!context.BaseItems.Any(e => e.Id == entity.Id))
+ if (!existingItems.Any(e => e == entity.Id))
{
context.BaseItems.Add(entity);
}
else
{
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+ context.BaseItemImageInfos.Where(e => e.ItemId == entity.Id).ExecuteDelete();
+
+ if (entity.Images is { Count: > 0 })
+ {
+ context.BaseItemImageInfos.AddRange(entity.Images);
+ }
+
context.BaseItems.Attach(entity).State = EntityState.Modified;
}
+ }
- context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete();
- if (item.Item.SupportsAncestors && item.AncestorIds != null)
+ context.SaveChanges();
+
+ foreach (var item in newItems)
+ {
+ // reattach old userData entries
+ var userKeys = item.UserDataKey.ToArray();
+ var retentionDate = (DateTime?)null;
+ context.UserData
+ .Where(e => e.ItemId == PlaceholderId)
+ .Where(e => userKeys.Contains(e.CustomDataKey))
+ .ExecuteUpdate(e => e
+ .SetProperty(f => f.ItemId, item.Item.Id)
+ .SetProperty(f => f.RetentionDate, retentionDate));
+ }
+
+ var itemValueMaps = tuples
+ .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
+ .ToArray();
+ var allListedItemValues = itemValueMaps
+ .SelectMany(f => f.Values)
+ .Distinct()
+ .ToArray();
+ var existingValues = context.ItemValues
+ .Select(e => new
{
- foreach (var ancestorId in item.AncestorIds)
- {
- if (!context.BaseItems.Any(f => f.Id == ancestorId))
- {
- continue;
- }
+ item = e,
+ Key = e.Type + "+" + e.Value
+ })
+ .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key))
+ .Select(e => e.item)
+ .ToArray();
+ var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue()
+ {
+ CleanValue = GetCleanValue(f.Value),
+ ItemValueId = Guid.NewGuid(),
+ Type = f.MagicNumber,
+ Value = f.Value
+ }).ToArray();
+ context.ItemValues.AddRange(missingItemValues);
+ context.SaveChanges();
+
+ var itemValuesStore = existingValues.Concat(missingItemValues).ToArray();
+ var valueMap = itemValueMaps
+ .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).DistinctBy(e => e.ItemValueId).ToArray()))
+ .ToArray();
- context.AncestorIds.Add(new AncestorId()
+ var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList();
+
+ foreach (var item in valueMap)
+ {
+ var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList();
+ foreach (var itemValue in item.Values)
+ {
+ var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId);
+ if (existingItem is null)
+ {
+ context.ItemValuesMap.Add(new ItemValueMap()
{
- ParentItemId = ancestorId,
- ItemId = entity.Id,
Item = null!,
- ParentItem = null!
+ ItemId = item.Item.Id,
+ ItemValue = null!,
+ ItemValueId = itemValue.ItemValueId
});
}
- }
-
- // Never save duplicate itemValues as they are now mapped anyway.
- var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags).DistinctBy(e => (GetCleanValue(e.Value), e.MagicNumber));
- context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete();
- foreach (var itemValue in itemValuesToSave)
- {
- if (!localItemValueCache.TryGetValue(itemValue, out var refValue))
+ else
{
- refValue = context.ItemValues
- .Where(f => f.Value == itemValue.Value && (int)f.Type == itemValue.MagicNumber)
- .Select(e => e.ItemValueId)
- .FirstOrDefault();
+ // map exists, remove from list so its been handled.
+ itemMappedValues.Remove(existingItem);
}
+ }
+
+ // all still listed values are not in the new list so remove them.
+ context.ItemValuesMap.RemoveRange(itemMappedValues);
+ }
+
+ context.SaveChanges();
- if (refValue.IsEmpty())
+ foreach (var item in tuples)
+ {
+ if (item.Item.SupportsAncestors && item.AncestorIds != null)
+ {
+ var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList();
+ var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).ToArray();
+ foreach (var ancestorId in validAncestorIds)
{
- context.ItemValues.Add(new ItemValue()
+ var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId);
+ if (existingAncestorId is null)
{
- CleanValue = GetCleanValue(itemValue.Value),
- Type = (ItemValueType)itemValue.MagicNumber,
- ItemValueId = refValue = Guid.NewGuid(),
- Value = itemValue.Value
- });
- localItemValueCache[itemValue] = refValue;
+ context.AncestorIds.Add(new AncestorId()
+ {
+ ParentItemId = ancestorId,
+ ItemId = item.Item.Id,
+ Item = null!,
+ ParentItem = null!
+ });
+ }
+ else
+ {
+ existingAncestorIds.Remove(existingAncestorId);
+ }
}
- context.ItemValuesMap.Add(new ItemValueMap()
- {
- Item = null!,
- ItemId = entity.Id,
- ItemValue = null!,
- ItemValueId = refValue
- });
+ context.AncestorIds.RemoveRange(existingAncestorIds);
}
}
@@ -575,19 +747,26 @@ public sealed class BaseItemRepository
}
using var context = _dbProvider.CreateDbContext();
- var item = PrepareItemQuery(context, new()
+ var dbQuery = PrepareItemQuery(context, new()
{
DtoOptions = new()
{
EnableImages = true
}
- }).FirstOrDefault(e => e.Id == id);
+ });
+ dbQuery = dbQuery.Include(e => e.TrailerTypes)
+ .Include(e => e.Provider)
+ .Include(e => e.LockedFields)
+ .Include(e => e.UserData)
+ .Include(e => e.Images);
+
+ var item = dbQuery.FirstOrDefault(e => e.Id == id);
if (item is null)
{
return null;
}
- return DeserialiseBaseItem(item);
+ return DeserializeBaseItem(item);
}
/// <summary>
@@ -596,8 +775,9 @@ public sealed class BaseItemRepository
/// <param name="entity">The entity.</param>
/// <param name="dto">The dto base instance.</param>
/// <param name="appHost">The Application server Host.</param>
+ /// <param name="logger">The applogger.</param>
/// <returns>The dto to map.</returns>
- public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost)
+ public static BaseItemDto Map(BaseItemEntity entity, BaseItemDto dto, IServerApplicationHost? appHost, ILogger logger)
{
dto.Id = entity.Id;
dto.ParentId = entity.ParentId.GetValueOrDefault();
@@ -633,15 +813,17 @@ public sealed class BaseItemRepository
dto.TotalBitrate = entity.TotalBitrate;
dto.ExternalId = entity.ExternalId;
dto.Size = entity.Size;
- dto.Genres = entity.Genres?.Split('|') ?? [];
- dto.DateCreated = entity.DateCreated.GetValueOrDefault();
- dto.DateModified = entity.DateModified.GetValueOrDefault();
+ dto.Genres = string.IsNullOrWhiteSpace(entity.Genres) ? [] : entity.Genres.Split('|');
+ dto.DateCreated = entity.DateCreated ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
+ dto.DateModified = entity.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.ChannelId = entity.ChannelId ?? Guid.Empty;
- dto.DateLastRefreshed = entity.DateLastRefreshed.GetValueOrDefault();
- dto.DateLastSaved = entity.DateLastSaved.GetValueOrDefault();
+ dto.DateLastRefreshed = entity.DateLastRefreshed ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
+ dto.DateLastSaved = entity.DateLastSaved ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
dto.OwnerId = string.IsNullOrWhiteSpace(entity.OwnerId) ? Guid.Empty : (Guid.TryParse(entity.OwnerId, out var ownerId) ? ownerId : Guid.Empty);
dto.Width = entity.Width.GetValueOrDefault();
dto.Height = entity.Height.GetValueOrDefault();
+ dto.UserData = entity.UserData;
+
if (entity.Provider is not null)
{
dto.ProviderIds = entity.Provider.ToDictionary(e => e.ProviderId, e => e.ProviderValue);
@@ -665,7 +847,7 @@ public sealed class BaseItemRepository
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
dto.Studios = entity.Studios?.Split('|') ?? [];
- dto.Tags = entity.Tags?.Split('|') ?? [];
+ dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
if (dto is IHasProgramAttributes hasProgramAttributes)
{
@@ -739,7 +921,7 @@ public sealed class BaseItemRepository
if (dto is Folder folder)
{
- folder.DateLastMediaAdded = entity.DateLastMediaAdded;
+ folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc);
}
return dto;
@@ -799,11 +981,11 @@ public sealed class BaseItemRepository
entity.ExternalId = dto.ExternalId;
entity.Size = dto.Size;
entity.Genres = string.Join('|', dto.Genres);
- entity.DateCreated = dto.DateCreated;
- entity.DateModified = dto.DateModified;
+ entity.DateCreated = dto.DateCreated == DateTime.MinValue ? null : dto.DateCreated;
+ entity.DateModified = dto.DateModified == DateTime.MinValue ? null : dto.DateModified;
entity.ChannelId = dto.ChannelId;
- entity.DateLastRefreshed = dto.DateLastRefreshed;
- entity.DateLastSaved = dto.DateLastSaved;
+ entity.DateLastRefreshed = dto.DateLastRefreshed == DateTime.MinValue ? null : dto.DateLastRefreshed;
+ entity.DateLastSaved = dto.DateLastSaved == DateTime.MinValue ? null : dto.DateLastSaved;
entity.OwnerId = dto.OwnerId.ToString();
entity.Width = dto.Width;
entity.Height = dto.Height;
@@ -913,7 +1095,7 @@ public sealed class BaseItemRepository
if (dto is Folder folder)
{
- entity.DateLastMediaAdded = folder.DateLastMediaAdded;
+ entity.DateLastMediaAdded = folder.DateLastMediaAdded == DateTime.MinValue ? null : folder.DateLastMediaAdded;
entity.IsFolder = folder.IsFolder;
}
@@ -949,7 +1131,7 @@ public sealed class BaseItemRepository
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
}
- private BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
+ private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
{
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
if (_serverConfigurationManager?.Configuration is null)
@@ -958,7 +1140,7 @@ public sealed class BaseItemRepository
}
var typeToSerialise = GetType(baseItemEntity.Type);
- return BaseItemRepository.DeserialiseBaseItem(
+ return BaseItemRepository.DeserializeBaseItem(
baseItemEntity,
_logger,
_appHost,
@@ -966,7 +1148,7 @@ public sealed class BaseItemRepository
}
/// <summary>
- /// Deserialises a BaseItemEntity and sets all properties.
+ /// Deserializes a BaseItemEntity and sets all properties.
/// </summary>
/// <param name="baseItemEntity">The DB entity.</param>
/// <param name="logger">Logger.</param>
@@ -974,9 +1156,9 @@ public sealed class BaseItemRepository
/// <param name="skipDeserialization">If only mapping should be processed.</param>
/// <returns>A mapped BaseItem.</returns>
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
- public static BaseItemDto DeserialiseBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
+ public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
{
- var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
+ var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
BaseItemDto? dto = null;
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
{
@@ -992,13 +1174,13 @@ public sealed class BaseItemRepository
if (dto is null)
{
- dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialise unknown type.");
+ dto = Activator.CreateInstance(type) as BaseItemDto ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
}
- return Map(baseItemEntity, dto, appHost);
+ return Map(baseItemEntity, dto, appHost, logger);
}
- 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);
@@ -1009,20 +1191,76 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext();
- var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter);
+ var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context, new InternalItemsQuery(filter.User)
+ {
+ ExcludeItemTypes = filter.ExcludeItemTypes,
+ IncludeItemTypes = filter.IncludeItemTypes,
+ 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
+ });
- 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)));
+ var itemValuesQuery = context.ItemValues
+ .Where(f => itemValueTypes.Contains(f.Type))
+ .SelectMany(f => f.BaseItemsMap!, (f, w) => new { f, w })
+ .Join(
+ innerQueryFilter,
+ fw => fw.w.ItemId,
+ g => g.Id,
+ (fw, g) => fw.f.CleanValue);
+
+ var innerQuery = PrepareItemQuery(context, filter)
+ .Where(e => e.Type == returnType)
+ .Where(e => itemValuesQuery.Contains(e.CleanName));
+
+ var outerQueryFilter = new InternalItemsQuery(filter.User)
+ {
+ 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
+ };
- if (filter.OrderBy.Count != 0
- || !string.IsNullOrEmpty(filter.SearchTerm))
- {
- query = ApplyOrder(query, filter);
- }
- else
+ var masterQuery = TranslateQuery(innerQuery, context, outerQueryFilter)
+ .GroupBy(e => e.PresentationUniqueKey)
+ .Select(e => e.FirstOrDefault())
+ .Select(e => e!.Id);
+
+ var query = context.BaseItems
+ .Include(e => e.TrailerTypes)
+ .Include(e => e.Provider)
+ .Include(e => e.LockedFields)
+ .Include(e => e.Images)
+ .AsSingleQuery()
+ .Where(e => masterQuery.Contains(e.Id));
+
+ query = ApplyOrder(query, filter, context);
+
+ var result = new QueryResult<(BaseItemDto, ItemCounts?)>();
+ if (filter.EnableTotalRecordCount)
{
- query = query.OrderBy(e => e.SortName);
+ result.TotalRecordCount = query.Count();
}
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
@@ -1040,41 +1278,78 @@ public sealed class BaseItemRepository
}
}
- var result = new QueryResult<(BaseItemDto, ItemCounts)>();
- if (filter.EnableTotalRecordCount)
- {
- result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count();
- }
-
- 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];
+ IQueryable<BaseItemEntity>? itemCountQuery = null;
- var resultQuery = query.Select(e => new
+ if (filter.IncludeItemTypes.Length > 0)
{
- item = e,
- // TODO: This is bad refactor!
- itemCount = new ItemCounts()
+ // if we are to include more then one type, sub query those items beforehand.
+
+ var typeSubQuery = new InternalItemsQuery(filter.User)
{
- SeriesCount = e.ItemValues!.Count(f => f.Item.Type == seriesTypeName),
- EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == episodeTypeName),
- MovieCount = e.ItemValues!.Count(f => f.Item.Type == movieTypeName),
- AlbumCount = e.ItemValues!.Count(f => f.Item.Type == musicAlbumTypeName),
- ArtistCount = e.ItemValues!.Count(f => f.Item.Type == musicArtistTypeName),
- SongCount = e.ItemValues!.Count(f => f.Item.Type == audioTypeName),
- TrailerCount = e.ItemValues!.Count(f => f.Item.Type == trailerTypeName),
- }
- });
+ 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
+ };
- result.StartIndex = filter.StartIndex ?? 0;
- result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e =>
+ 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 resultQuery = query.Select(e => new
+ {
+ item = e,
+ // 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 (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
+ })
+ ];
+ }
+ else
{
- return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
- }).ToArray();
+ result.StartIndex = filter.StartIndex ?? 0;
+ result.Items =
+ [
+ .. query
+ .AsEnumerable()
+ .Where(e => e is not null)
+ .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
+ {
+ return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
+ })
+ ];
+ }
return result;
}
@@ -1102,27 +1377,27 @@ public sealed class BaseItemRepository
return value.RemoveDiacritics().ToLowerInvariant();
}
- private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
+ private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List<string> inheritedTags)
{
- var list = new List<(int, string)>();
+ var list = new List<(ItemValueType, string)>();
if (item is IHasArtist hasArtist)
{
- list.AddRange(hasArtist.Artists.Select(i => (0, i)));
+ list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i)));
}
if (item is IHasAlbumArtist hasAlbumArtist)
{
- list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i)));
+ list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i)));
}
- list.AddRange(item.Genres.Select(i => (2, i)));
- list.AddRange(item.Studios.Select(i => (3, i)));
- list.AddRange(item.Tags.Select(i => (4, i)));
+ list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i)));
+ list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i)));
+ list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i)));
// keywords was 5
- list.AddRange(inheritedTags.Select(i => (6, i)));
+ list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i)));
// Remove all invalid values.
list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2));
@@ -1152,7 +1427,7 @@ public sealed class BaseItemRepository
{
Path = appHost?.ExpandVirtualPath(e.Path) ?? e.Path,
BlurHash = e.Blurhash is null ? null : Encoding.UTF8.GetString(e.Blurhash),
- DateModified = e.DateModified,
+ DateModified = e.DateModified ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc),
Height = e.Height,
Width = e.Width,
Type = (ImageType)e.ImageType
@@ -1246,7 +1521,7 @@ public sealed class BaseItemRepository
|| query.IncludeItemTypes.Contains(BaseItemKind.Season);
}
- private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter)
+ private IQueryable<BaseItemEntity> ApplyOrder(IQueryable<BaseItemEntity> query, InternalItemsQuery filter, JellyfinDbContext context)
{
var orderBy = filter.OrderBy;
var hasSearch = !string.IsNullOrEmpty(filter.SearchTerm);
@@ -1257,7 +1532,7 @@ public sealed class BaseItemRepository
}
else if (orderBy.Count == 0)
{
- return query;
+ return query.OrderBy(e => e.SortName);
}
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
@@ -1265,7 +1540,7 @@ public sealed class BaseItemRepository
var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default)
{
- var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
if (firstOrdering.SortOrder == SortOrder.Ascending)
{
orderedQuery = query.OrderBy(expression);
@@ -1290,7 +1565,7 @@ public sealed class BaseItemRepository
foreach (var item in orderBy.Skip(1))
{
- var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(item.OrderBy, filter, context);
if (item.SortOrder == SortOrder.Ascending)
{
orderedQuery = orderedQuery!.ThenBy(expression);
@@ -1356,7 +1631,7 @@ public sealed class BaseItemRepository
if (maxWidth.HasValue)
{
- baseQuery = baseQuery.Where(e => e.Width >= maxWidth);
+ baseQuery = baseQuery.Where(e => e.Width <= maxWidth);
}
if (filter.MaxHeight.HasValue)
@@ -1429,8 +1704,17 @@ public sealed class BaseItemRepository
if (!string.IsNullOrEmpty(filter.SearchTerm))
{
- var searchTerm = filter.SearchTerm.ToLower();
- baseQuery = baseQuery.Where(e => e.CleanName!.ToLower().Contains(searchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(searchTerm)));
+ var cleanedSearchTerm = GetCleanValue(filter.SearchTerm);
+ var originalSearchTerm = filter.SearchTerm.ToLower();
+ if (SearchWildcardTerms.Any(f => cleanedSearchTerm.Contains(f)))
+ {
+ cleanedSearchTerm = $"%{cleanedSearchTerm.Trim('%')}%";
+ baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName!, cleanedSearchTerm) || (e.OriginalTitle != null && EF.Functions.Like(e.OriginalTitle.ToLower(), originalSearchTerm)));
+ }
+ else
+ {
+ baseQuery = baseQuery.Where(e => e.CleanName!.Contains(cleanedSearchTerm) || (e.OriginalTitle != null && e.OriginalTitle.ToLower().Contains(originalSearchTerm)));
+ }
}
if (filter.IsFolder.HasValue)
@@ -1439,6 +1723,7 @@ public sealed class BaseItemRepository
}
var includeTypes = filter.IncludeItemTypes;
+
// Only specify excluded types if no included types are specified
if (filter.IncludeItemTypes.Length == 0)
{
@@ -1464,25 +1749,10 @@ public sealed class BaseItemRepository
baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
}
}
- else if (includeTypes.Length == 1)
- {
- if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
- {
- baseQuery = baseQuery.Where(e => e.Type == includeTypeName);
- }
- }
- else if (includeTypes.Length > 1)
+ else
{
- 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));
+ string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e => e != null).ToArray()!;
+ baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
}
if (filter.ChannelIds.Count > 0)
@@ -1497,7 +1767,8 @@ public sealed class BaseItemRepository
if (!string.IsNullOrWhiteSpace(filter.Path))
{
- baseQuery = baseQuery.Where(e => e.Path == filter.Path);
+ var pathToQuery = GetPathToSave(filter.Path);
+ baseQuery = baseQuery.Where(e => e.Path == pathToQuery);
}
if (!string.IsNullOrWhiteSpace(filter.PresentationUniqueKey))
@@ -1588,7 +1859,7 @@ public sealed class BaseItemRepository
if (filter.MinPremiereDate.HasValue)
{
- baseQuery = baseQuery.Where(e => e.PremiereDate <= filter.MinPremiereDate.Value);
+ baseQuery = baseQuery.Where(e => e.PremiereDate >= filter.MinPremiereDate.Value);
}
if (filter.MaxPremiereDate.HasValue)
@@ -1616,10 +1887,17 @@ public sealed class BaseItemRepository
if (filter.PersonIds.Length > 0)
{
+ var peopleEntityIds = context.BaseItems
+ .WhereOneOrMany(filter.PersonIds, b => b.Id)
+ .Join(
+ context.Peoples,
+ b => b.Name,
+ p => p.Name,
+ (b, p) => p.Id);
+
baseQuery = baseQuery
- .Where(e =>
- context.PeopleBaseItemMap.Where(w => context.BaseItems.Where(r => filter.PersonIds.Contains(r.Id)).Any(f => f.Name == w.People.Name))
- .Any(f => f.ItemId == e.Id));
+ .Where(e => context.PeopleBaseItemMap
+ .Any(m => m.ItemId == e.Id && peopleEntityIds.Contains(m.PeopleId)));
}
if (!string.IsNullOrWhiteSpace(filter.Person))
@@ -1655,26 +1933,35 @@ public sealed class BaseItemRepository
var nameContains = filter.NameContains;
if (!string.IsNullOrWhiteSpace(nameContains))
{
- baseQuery = baseQuery.Where(e =>
- e.CleanName!.Contains(nameContains)
- || e.OriginalTitle!.ToLower().Contains(nameContains!));
+ if (SearchWildcardTerms.Any(f => nameContains.Contains(f)))
+ {
+ nameContains = $"%{nameContains.Trim('%')}%";
+ baseQuery = baseQuery.Where(e => EF.Functions.Like(e.CleanName, nameContains) || EF.Functions.Like(e.OriginalTitle, nameContains));
+ }
+ else
+ {
+ baseQuery = baseQuery.Where(e =>
+ e.CleanName!.Contains(nameContains)
+ || e.OriginalTitle!.ToLower().Contains(nameContains!));
+ }
}
if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
{
- baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(filter.NameStartsWith) || e.Name!.StartsWith(filter.NameStartsWith));
+ var startsWithLower = filter.NameStartsWith.ToLowerInvariant();
+ baseQuery = baseQuery.Where(e => e.SortName!.StartsWith(startsWithLower));
}
if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
{
- // i hate this
- baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() > filter.NameStartsWithOrGreater[0] || e.Name!.FirstOrDefault() > filter.NameStartsWithOrGreater[0]);
+ var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant();
+ baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(startsOrGreaterLower) >= 0);
}
if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
{
- // i hate this
- baseQuery = baseQuery.Where(e => e.SortName!.FirstOrDefault() < filter.NameLessThan[0] || e.Name!.FirstOrDefault() < filter.NameLessThan[0]);
+ var lessThanLower = filter.NameLessThan.ToLowerInvariant();
+ baseQuery = baseQuery.Where(e => e.SortName!.CompareTo(lessThanLower ) < 0);
}
if (filter.ImageTypes.Length > 0)
@@ -1706,7 +1993,7 @@ public sealed class BaseItemRepository
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
{
- baseQuery = baseQuery.Where(e => context.BaseItems
+ baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
.Where(e => e.IsFolder == false && e.IsVirtualItem == false)
.Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
.Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
@@ -1740,64 +2027,70 @@ public sealed class BaseItemRepository
if (filter.ArtistIds.Length > 0)
{
- baseQuery = baseQuery
- .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId)));
+ baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ArtistIds);
}
if (filter.AlbumArtistIds.Length > 0)
{
- baseQuery = baseQuery
- .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId)));
+ baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.AlbumArtist, filter.AlbumArtistIds);
}
if (filter.ContributingArtistIds.Length > 0)
{
- baseQuery = baseQuery
- .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId)));
+ var contributingNames = context.BaseItems
+ .Where(b => filter.ContributingArtistIds.Contains(b.Id))
+ .Select(b => b.CleanName);
+
+ baseQuery = baseQuery.Where(e =>
+ e.ItemValues!.Any(ivm =>
+ ivm.ItemValue.Type == ItemValueType.Artist &&
+ contributingNames.Contains(ivm.ItemValue.CleanValue))
+ &&
+ !e.ItemValues!.Any(ivm =>
+ ivm.ItemValue.Type == ItemValueType.AlbumArtist &&
+ contributingNames.Contains(ivm.ItemValue.CleanValue)));
}
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)
{
- baseQuery = baseQuery
- .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId)));
+ baseQuery = baseQuery.WhereReferencedItemMultipleTypes(context, [ItemValueType.Artist, ItemValueType.AlbumArtist], filter.ExcludeArtistIds, true);
}
if (filter.GenreIds.Count > 0)
{
- baseQuery = baseQuery
- .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId)));
+ baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray());
}
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
- .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)
{
- 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
- .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)
{
- 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
- .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)
{
- baseQuery = baseQuery
- .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId)));
+ baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray());
}
if (filter.OfficialRatings.Length > 0)
@@ -1810,22 +2103,26 @@ public sealed class BaseItemRepository
if (filter.MinParentalRating != null)
{
var min = filter.MinParentalRating;
- minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
- if (min.SubScore != null)
- {
- minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
- }
+ var minScore = min.Score;
+ var minSubScore = min.SubScore ?? 0;
+
+ minParentalRatingFilter = e =>
+ e.InheritedParentalRatingValue == null ||
+ e.InheritedParentalRatingValue > minScore ||
+ (e.InheritedParentalRatingValue == minScore && (e.InheritedParentalRatingSubValue ?? 0) >= minSubScore);
}
Expression<Func<BaseItemEntity, bool>>? maxParentalRatingFilter = null;
if (filter.MaxParentalRating != null)
{
var max = filter.MaxParentalRating;
- maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null;
- if (max.SubScore != null)
- {
- maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
- }
+ var maxScore = max.Score;
+ var maxSubScore = max.SubScore ?? 0;
+
+ maxParentalRatingFilter = e =>
+ e.InheritedParentalRatingValue == null ||
+ e.InheritedParentalRatingValue < maxScore ||
+ (e.InheritedParentalRatingValue == maxScore && (e.InheritedParentalRatingSubValue ?? 0) <= maxSubScore);
}
if (filter.HasParentalRating ?? false)
@@ -1961,7 +2258,7 @@ public sealed class BaseItemRepository
if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
{
baseQuery = baseQuery
- .Where(e => e.ParentId.HasValue && !context.BaseItems.Any(f => f.Id == e.ParentId.Value));
+ .Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any(f => f.Id == e.ParentId.Value));
}
if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
@@ -1988,15 +2285,9 @@ public sealed class BaseItemRepository
.Where(e => !context.Peoples.Any(f => f.Name == e.Name));
}
- if (filter.Years.Length == 1)
- {
- baseQuery = baseQuery
- .Where(e => e.ProductionYear == filter.Years[0]);
- }
- else if (filter.Years.Length > 1)
+ if (filter.Years.Length > 0)
{
- baseQuery = baseQuery
- .Where(e => filter.Years.Any(f => f == e.ProductionYear));
+ baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value);
}
var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing;
@@ -2037,45 +2328,61 @@ public sealed class BaseItemRepository
if (filter.MediaTypes.Length > 0)
{
var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray();
- baseQuery = baseQuery
- .Where(e => mediaTypes.Contains(e.MediaType));
+ baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType);
}
if (filter.ItemIds.Length > 0)
{
- baseQuery = baseQuery
- .Where(e => filter.ItemIds.Contains(e.Id));
+ baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id);
}
if (filter.ExcludeItemIds.Length > 0)
{
baseQuery = baseQuery
- .Where(e => !filter.ItemIds.Contains(e.Id));
+ .Where(e => !filter.ExcludeItemIds.Contains(e.Id));
}
if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
{
- baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
+ var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
+ baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !exclude.Contains(f)));
}
if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
{
- baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
+ // Allow setting a null or empty value to get all items that have the specified provider set.
+ var includeAny = filter.HasAnyProviderId.Where(e => string.IsNullOrEmpty(e.Value)).Select(e => e.Key).ToArray();
+ if (includeAny.Length > 0)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Any(f => includeAny.Contains(f.ProviderId)));
+ }
+
+ var includeSelected = filter.HasAnyProviderId.Where(e => !string.IsNullOrEmpty(e.Value)).Select(e => $"{e.Key}:{e.Value}").ToArray();
+ if (includeSelected.Length > 0)
+ {
+ baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => includeSelected.Contains(f)));
+ }
}
if (filter.HasImdbId.HasValue)
{
- baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "imdb"));
+ baseQuery = filter.HasImdbId.Value
+ ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Imdb.ToString().ToLower()))
+ : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Imdb.ToString().ToLower()));
}
if (filter.HasTmdbId.HasValue)
{
- baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tmdb"));
+ baseQuery = filter.HasTmdbId.Value
+ ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tmdb.ToString().ToLower()))
+ : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tmdb.ToString().ToLower()));
}
if (filter.HasTvdbId.HasValue)
{
- baseQuery = baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId == "tvdb"));
+ baseQuery = filter.HasTvdbId.Value
+ ? baseQuery.Where(e => e.Provider!.Any(f => f.ProviderId.ToLower() == MetadataProvider.Tvdb.ToString().ToLower()))
+ : baseQuery.Where(e => e.Provider!.All(f => f.ProviderId.ToLower() != MetadataProvider.Tvdb.ToString().ToLower()));
}
var queryTopParentIds = filter.TopParentIds;
@@ -2090,19 +2397,19 @@ public sealed class BaseItemRepository
}
else
{
- baseQuery = baseQuery.Where(e => queryTopParentIds.Contains(e.TopParentId!.Value));
+ baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value);
}
}
if (filter.AncestorIds.Length > 0)
{
- baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
+ baseQuery = baseQuery.Where(e => e.Parents!.Any(f => filter.AncestorIds.Contains(f.ParentItemId)));
}
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
{
baseQuery = baseQuery
- .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
+ .Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
}
if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
@@ -2113,40 +2420,34 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0)
{
- baseQuery = baseQuery
- .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
- .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
+ baseQuery = baseQuery.Where(e =>
+ !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
+ && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
+ !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
if (filter.IncludeInheritedTags.Length > 0)
{
- // Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
- // In addition to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
- if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
+ // For seasons and episodes, we also need to check the parent series' tags.
+ if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
{
- baseQuery = baseQuery
- .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
- .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
- ||
- (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value)!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags)
- .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
+ baseQuery = baseQuery.Where(e =>
+ e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
+ || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
// A playlist should be accessible to its owner regardless of allowed tags.
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
{
- baseQuery = baseQuery
- .Where(e =>
- e.Parents!
- .Any(f =>
- f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))
- || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
+ baseQuery = baseQuery.Where(e =>
+ e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
+ || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
// d ^^ this is stupid it hate this.
}
else
{
- baseQuery = baseQuery
- .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
+ baseQuery = baseQuery.Where(e =>
+ e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
}
@@ -2229,4 +2530,78 @@ public sealed class BaseItemRepository
return baseQuery;
}
+
+ /// <inheritdoc/>
+ public async Task<bool> ItemExistsAsync(Guid id)
+ {
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
+ }
+ }
+
+ /// <inheritdoc/>
+ public bool GetIsPlayed(User user, Guid id, bool recursive)
+ {
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ if (recursive)
+ {
+ var folderList = TraverseHirachyDown(id, dbContext, item => (item.IsFolder || item.IsVirtualItem));
+
+ return dbContext.BaseItems
+ .Where(e => folderList.Contains(e.ParentId!.Value) && !e.IsFolder && !e.IsVirtualItem)
+ .All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
+ }
+
+ return dbContext.BaseItems.Where(e => e.ParentId == id).All(f => f.UserData!.Any(e => e.UserId == user.Id && e.Played));
+ }
+
+ private static HashSet<Guid> TraverseHirachyDown(Guid parentId, JellyfinDbContext dbContext, Expression<Func<BaseItemEntity, bool>>? filter = null)
+ {
+ var folderStack = new HashSet<Guid>()
+ {
+ parentId
+ };
+ var folderList = new HashSet<Guid>()
+ {
+ parentId
+ };
+
+ while (folderStack.Count != 0)
+ {
+ var items = folderStack.ToArray();
+ folderStack.Clear();
+ var query = dbContext.BaseItems
+ .WhereOneOrMany(items, e => e.ParentId!.Value);
+
+ if (filter != null)
+ {
+ query = query.Where(filter);
+ }
+
+ foreach (var item in query.Select(e => e.Id).ToArray())
+ {
+ if (folderList.Add(item))
+ {
+ folderStack.Add(item);
+ }
+ }
+ }
+
+ return folderList;
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyDictionary<string, MusicArtist[]> FindArtists(IReadOnlyList<string> artistNames)
+ {
+ using var dbContext = _dbProvider.CreateDbContext();
+
+ var artists = dbContext.BaseItems.Where(e => e.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]!)
+ .Where(e => artistNames.Contains(e.Name))
+ .ToArray();
+
+ return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
+ }
}