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.cs807
1 files changed, 508 insertions, 299 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 392b7de74..d59eba690 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -7,19 +7,21 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
-using Jellyfin.Data.Entities;
+using System.Threading.Tasks;
using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.Server.Implementations.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
@@ -36,7 +38,7 @@ using MediaBrowser.Model.Querying;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem;
-using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity;
+using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity;
namespace Jellyfin.Server.Implementations.Item;
@@ -53,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>
@@ -67,7 +74,7 @@ public sealed class BaseItemRepository
private static readonly IReadOnlyList<ItemValueType> _getArtistValueTypes = [ItemValueType.Artist];
private static readonly IReadOnlyList<ItemValueType> _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist];
private static readonly IReadOnlyList<ItemValueType> _getStudiosValueTypes = [ItemValueType.Studios];
- private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Studios];
+ private static readonly IReadOnlyList<ItemValueType> _getGenreValueTypes = [ItemValueType.Genre];
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
@@ -94,23 +101,53 @@ public sealed class BaseItemRepository
/// <inheritdoc />
public void DeleteItem(Guid id)
{
- if (id.IsEmpty())
+ if (id.IsEmpty() || id.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(id));
}
using var context = _dbProvider.CreateDbContext();
using var transaction = context.Database.BeginTransaction();
- context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete();
- context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete();
- context.Chapters.Where(e => e.ItemId == id).ExecuteDelete();
- context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
+
+ var date = (DateTime?)DateTime.UtcNow;
+
+ // 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.Where(e => e.ItemId == id),
+ 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.Where(e => e.ItemId == id)
+ .ExecuteUpdate(e => e
+ .SetProperty(f => f.RetentionDate, date)
+ .SetProperty(f => f.ItemId, PlaceholderId));
+
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
- context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete();
- context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).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();
+ 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.SaveChanges();
transaction.Commit();
}
@@ -135,41 +172,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]);
}
@@ -233,7 +270,7 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyQueryPaging(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;
}
@@ -252,7 +289,80 @@ public sealed class BaseItemRepository
dbQuery = ApplyGroupingFilter(dbQuery, filter);
dbQuery = ApplyQueryPaging(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/>
+ public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ PrepareFilterQuery(filter);
+
+ // Early exit if collection type is not tvshows or music
+ if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music)
+ {
+ return Array.Empty<BaseItem>();
+ }
+
+ using var context = _dbProvider.CreateDbContext();
+
+ // Subquery to group by SeriesNames/Album and get the max Date Created for each group.
+ var subquery = PrepareItemQuery(context, filter);
+ subquery = TranslateQuery(subquery, context, filter);
+ var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album)
+ .Select(g => new
+ {
+ Key = g.Key,
+ MaxDateCreated = g.Max(a => a.DateCreated)
+ })
+ .OrderByDescending(g => g.MaxDateCreated)
+ .Select(g => g);
+
+ if (filter.Limit.HasValue)
+ {
+ subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value);
+ }
+
+ filter.Limit = null;
+
+ 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 = ApplyQueryPaging(mainquery, filter);
+
+ return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
+ }
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff)
+ {
+ ArgumentNullException.ThrowIfNull(filter);
+ ArgumentNullException.ThrowIfNull(filter.User);
+
+ using var context = _dbProvider.CreateDbContext();
+
+ var query = context.BaseItems
+ .AsNoTracking()
+ .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
+ .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
+ .Join(
+ 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 })
+ .GroupBy(g => g.Item.SeriesPresentationUniqueKey)
+ .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) })
+ .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff)
+ .OrderByDescending(g => g.LastPlayedDate)
+ .Select(g => g.Key!);
+
+ if (filter.Limit.HasValue)
+ {
+ query = query.Take(filter.Limit.Value);
+ }
+
+ return query.ToArray();
}
private IQueryable<BaseItemEntity> ApplyGroupingFilter(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
@@ -320,7 +430,8 @@ public sealed class BaseItemRepository
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.Provider)
.Include(e => e.LockedFields);
@@ -371,11 +482,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 />
@@ -391,7 +507,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() :
@@ -405,17 +521,20 @@ 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);
}
@@ -424,59 +543,111 @@ public sealed class BaseItemRepository
context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete();
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();
- context.AncestorIds.Add(new AncestorId()
+ 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();
+
+ 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.CleanValue == GetCleanValue(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);
+ }
- if (refValue.IsEmpty())
+ context.SaveChanges();
+
+ 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);
}
}
@@ -505,7 +676,7 @@ public sealed class BaseItemRepository
return null;
}
- return DeserialiseBaseItem(item);
+ return DeserializeBaseItem(item);
}
/// <summary>
@@ -538,6 +709,7 @@ public sealed class BaseItemRepository
dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode;
dto.IsInMixedFolder = entity.IsInMixedFolder;
dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue;
+ dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue;
dto.CriticRating = entity.CriticRating;
dto.PresentationUniqueKey = entity.PresentationUniqueKey;
dto.OriginalTitle = entity.OriginalTitle;
@@ -550,12 +722,12 @@ 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();
@@ -582,7 +754,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)
{
@@ -656,7 +828,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;
@@ -702,6 +874,7 @@ public sealed class BaseItemRepository
entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode;
entity.IsInMixedFolder = dto.IsInMixedFolder;
entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue;
+ entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue;
entity.CriticRating = dto.CriticRating;
entity.PresentationUniqueKey = dto.PresentationUniqueKey;
entity.OriginalTitle = dto.OriginalTitle;
@@ -715,11 +888,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;
@@ -829,7 +1002,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;
}
@@ -865,7 +1038,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)
@@ -874,7 +1047,7 @@ public sealed class BaseItemRepository
}
var typeToSerialise = GetType(baseItemEntity.Type);
- return BaseItemRepository.DeserialiseBaseItem(
+ return BaseItemRepository.DeserializeBaseItem(
baseItemEntity,
_logger,
_appHost,
@@ -882,7 +1055,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>
@@ -890,9 +1063,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)
{
@@ -908,13 +1081,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);
}
- 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);
@@ -925,7 +1098,7 @@ public sealed class BaseItemRepository
using var context = _dbProvider.CreateDbContext();
- var innerQuery = new InternalItemsQuery(filter.User)
+ var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context, new InternalItemsQuery(filter.User)
{
ExcludeItemTypes = filter.ExcludeItemTypes,
IncludeItemTypes = filter.IncludeItemTypes,
@@ -940,19 +1113,44 @@ public sealed class BaseItemRepository
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)
+ {
+ 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(context.BaseItems.AsNoTracking(), context, innerQuery);
- query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type)));
+ var query = TranslateQuery(innerQuery, context, outerQueryFilter)
+ .GroupBy(e => e.PresentationUniqueKey);
- if (filter.OrderBy.Count != 0
- || !string.IsNullOrEmpty(filter.SearchTerm))
- {
- query = ApplyOrder(query, filter);
- }
- else
+ 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)
@@ -970,41 +1168,84 @@ 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();
- }
+ IQueryable<BaseItemEntity>? itemCountQuery = null;
- 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
+ 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.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 (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
+ .Select(e => e.First())
+ .AsEnumerable()
+ .Where(e => e is not null)
+ .Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
+ {
+ return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
+ })
+ ];
+ }
return result;
}
@@ -1032,27 +1273,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));
@@ -1082,7 +1323,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
@@ -1141,45 +1382,6 @@ public sealed class BaseItemRepository
return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
}
- private Expression<Func<BaseItemEntity, object>> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
- {
-#pragma warning disable CS8603 // Possible null reference return.
- return sortBy switch
- {
- ItemSortBy.AirTime => e => e.SortName, // TODO
- ItemSortBy.Runtime => e => e.RunTimeTicks,
- ItemSortBy.Random => e => EF.Functions.Random(),
- ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.LastPlayedDate,
- ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount,
- ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite,
- ItemSortBy.IsFolder => e => e.IsFolder,
- ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
- ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played,
- ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded,
- ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
- ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
- ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(),
- ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue,
- // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
- ItemSortBy.SeriesSortName => e => e.SeriesName,
- // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
- ItemSortBy.Album => e => e.Album,
- ItemSortBy.DateCreated => e => e.DateCreated,
- ItemSortBy.PremiereDate => e => e.PremiereDate,
- ItemSortBy.StartDate => e => e.StartDate,
- ItemSortBy.Name => e => e.Name,
- ItemSortBy.CommunityRating => e => e.CommunityRating,
- ItemSortBy.ProductionYear => e => e.ProductionYear,
- ItemSortBy.CriticRating => e => e.CriticRating,
- ItemSortBy.VideoBitRate => e => e.TotalBitrate,
- ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber,
- ItemSortBy.IndexNumber => e => e.IndexNumber,
- _ => e => e.SortName
- };
-#pragma warning restore CS8603 // Possible null reference return.
-
- }
-
private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query)
{
if (!query.GroupByPresentationUniqueKey)
@@ -1226,7 +1428,7 @@ public sealed class BaseItemRepository
}
else if (orderBy.Count == 0)
{
- return query;
+ return query.OrderBy(e => e.SortName);
}
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
@@ -1234,7 +1436,7 @@ public sealed class BaseItemRepository
var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default)
{
- var expression = MapOrderByField(firstOrdering.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter);
if (firstOrdering.SortOrder == SortOrder.Ascending)
{
orderedQuery = query.OrderBy(expression);
@@ -1259,7 +1461,7 @@ public sealed class BaseItemRepository
foreach (var item in orderBy.Skip(1))
{
- var expression = MapOrderByField(item.OrderBy, filter);
+ var expression = OrderMapper.MapOrderByField(item.OrderBy, filter);
if (item.SortOrder == SortOrder.Ascending)
{
orderedQuery = orderedQuery!.ThenBy(expression);
@@ -1278,34 +1480,39 @@ public sealed class BaseItemRepository
JellyfinDbContext context,
InternalItemsQuery filter)
{
+ const int HDWidth = 1200;
+ const int UHDWidth = 3800;
+ const int UHDHeight = 2100;
+
var minWidth = filter.MinWidth;
var maxWidth = filter.MaxWidth;
var now = DateTime.UtcNow;
- if (filter.IsHD.HasValue)
+ if (filter.IsHD.HasValue || filter.Is4K.HasValue)
{
- const int Threshold = 1200;
- if (filter.IsHD.Value)
- {
- minWidth = Threshold;
- }
- else
+ bool includeSD = false;
+ bool includeHD = false;
+ bool include4K = false;
+
+ if (filter.IsHD.HasValue && !filter.IsHD.Value)
{
- maxWidth = Threshold - 1;
+ includeSD = true;
}
- }
- if (filter.Is4K.HasValue)
- {
- const int Threshold = 3800;
- if (filter.Is4K.Value)
+ if (filter.IsHD.HasValue && filter.IsHD.Value)
{
- minWidth = Threshold;
+ includeHD = true;
}
- else
+
+ if (filter.Is4K.HasValue && filter.Is4K.Value)
{
- maxWidth = Threshold - 1;
+ include4K = true;
}
+
+ baseQuery = baseQuery.Where(e =>
+ (includeSD && e.Width < HDWidth) ||
+ (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) ||
+ (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)));
}
if (minWidth.HasValue)
@@ -1320,7 +1527,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)
@@ -1403,6 +1610,7 @@ public sealed class BaseItemRepository
}
var includeTypes = filter.IncludeItemTypes;
+
// Only specify excluded types if no included types are specified
if (filter.IncludeItemTypes.Length == 0)
{
@@ -1428,25 +1636,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)
@@ -1552,7 +1745,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)
@@ -1670,7 +1863,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);
@@ -1704,64 +1897,59 @@ 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.WhereReferencedItem(context, ItemValueType.Artist, 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.Artist, 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)));
+ baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds);
}
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.WhereReferencedItem(context, ItemValueType.Artist, 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)
@@ -1770,61 +1958,73 @@ public sealed class BaseItemRepository
.Where(e => filter.OfficialRatings.Contains(e.OfficialRating));
}
- if (filter.HasParentalRating ?? false)
+ Expression<Func<BaseItemEntity, bool>>? minParentalRatingFilter = null;
+ if (filter.MinParentalRating != null)
{
- if (filter.MinParentalRating.HasValue)
+ var min = filter.MinParentalRating;
+ minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null;
+ if (min.SubScore != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null);
}
+ }
- if (filter.MaxParentalRating.HasValue)
+ 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)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value);
+ maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null);
}
}
- else if (filter.BlockUnratedItems.Length > 0)
+
+ if (filter.HasParentalRating ?? false)
{
- var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
- if (filter.MinParentalRating.HasValue)
+ if (minParentalRatingFilter != null)
{
- if (filter.MaxParentalRating.HasValue)
- {
- baseQuery = baseQuery
- .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
- || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating));
- }
- else
- {
- baseQuery = baseQuery
- .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType))
- || e.InheritedParentalRatingValue >= filter.MinParentalRating);
- }
+ baseQuery = baseQuery.Where(minParentalRatingFilter);
}
- else
+
+ if (maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType));
+ baseQuery = baseQuery.Where(maxParentalRatingFilter);
}
}
- else if (filter.MinParentalRating.HasValue)
+ else if (filter.BlockUnratedItems.Length > 0)
{
- if (filter.MaxParentalRating.HasValue)
+ var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray();
+ Expression<Func<BaseItemEntity, bool>> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType);
+
+ if (minParentalRatingFilter != null && maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value);
+ baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter)));
+ }
+ else if (minParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter));
+ }
+ else if (maxParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter));
}
else
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value);
+ baseQuery = baseQuery.Where(unratedItemFilter);
}
}
- else if (filter.MaxParentalRating.HasValue)
+ else if (minParentalRatingFilter != null || maxParentalRatingFilter != null)
{
- baseQuery = baseQuery
- .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value);
+ if (minParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(minParentalRatingFilter);
+ }
+
+ if (maxParentalRatingFilter != null)
+ {
+ baseQuery = baseQuery.Where(maxParentalRatingFilter);
+ }
}
else if (!filter.HasParentalRating ?? false)
{
@@ -1913,36 +2113,36 @@ 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)
{
baseQuery = baseQuery
- .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist) == 1);
+ .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
}
if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value)
{
baseQuery = baseQuery
- .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1);
+ .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
}
- if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
+ if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value)
{
baseQuery = baseQuery
- .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
+ .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name));
}
- if (filter.Years.Length == 1)
+ if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value)
{
baseQuery = baseQuery
- .Where(e => e.ProductionYear == filter.Years[0]);
+ .Where(e => !context.Peoples.Any(f => f.Name == e.Name));
}
- 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;
@@ -1983,30 +2183,30 @@ 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)));
+ var include = filter.HasAnyProviderId.Select(e => $"{e.Key}:{e.Value}").ToArray();
+ baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => include.Contains(f)));
}
if (filter.HasImdbId.HasValue)
@@ -2036,19 +2236,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.ParentAncestors!.Any(w => w.ItemId == f.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))
@@ -2060,8 +2260,8 @@ 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)));
+ .Where(e => !e.ItemValues!.Where(w => w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags)
+ .Any(f => filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
if (filter.IncludeInheritedTags.Length > 0)
@@ -2071,10 +2271,10 @@ public sealed class BaseItemRepository
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
{
baseQuery = baseQuery
- .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags)
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
.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)
+ (e.ParentId.HasValue && context.ItemValuesMap.Where(w => w.ItemId == e.ParentId.Value && (w.ItemValue.Type == ItemValueType.InheritedTags || w.ItemValue.Type == ItemValueType.Tags))
.Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
}
@@ -2082,17 +2282,16 @@ public sealed class BaseItemRepository
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
{
baseQuery = baseQuery
- .Where(e =>
- e.ParentAncestors!
- .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}\"")));
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
+ .Any(f => 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.ParentAncestors!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue))));
+ .Where(e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.InheritedTags || f.ItemValue.Type == ItemValueType.Tags)
+ .Any(f => filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
}
}
@@ -2175,4 +2374,14 @@ 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);
+ }
+ }
}