diff options
Diffstat (limited to 'Jellyfin.Server.Implementations/Item')
6 files changed, 475 insertions, 256 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b0a36b3ae..3efcb6dd3 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -9,6 +9,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Text.Json; @@ -19,6 +20,7 @@ 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; @@ -66,7 +68,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. @@ -112,6 +114,7 @@ public sealed class BaseItemRepository 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(); @@ -145,37 +148,37 @@ public sealed class BaseItemRepository } /// <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]); } @@ -399,7 +402,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); @@ -450,11 +454,9 @@ 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(); context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); context.BaseItemImageInfos.AddRange(images); context.SaveChanges(); - transaction.Commit(); } /// <inheritdoc /> @@ -484,17 +486,19 @@ 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(); + 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); } @@ -503,59 +507,98 @@ 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(); + + var itemValueMaps = tuples + .Select(e => (Item: 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 => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).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.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); + } + + 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); } } @@ -617,6 +660,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; @@ -781,6 +825,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; @@ -993,7 +1038,7 @@ public sealed class BaseItemRepository 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); @@ -1004,20 +1049,59 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); - var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); + var innerQueryFilter = TranslateQuery(context.BaseItems, 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 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 + }; - if (filter.OrderBy.Count != 0 - || !string.IsNullOrEmpty(filter.SearchTerm)) - { - query = ApplyOrder(query, filter); - } - else + var query = TranslateQuery(innerQuery, context, outerQueryFilter) + .GroupBy(e => e.PresentationUniqueKey); + + 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) @@ -1035,41 +1119,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(); - } - - 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(), 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 (DeserialiseBaseItem(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 (DeserialiseBaseItem(e, filter.SkipDeserialization), null); + }) + ]; + } return result; } @@ -1097,27 +1224,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)); @@ -1252,7 +1379,7 @@ public sealed class BaseItemRepository } else if (orderBy.Count == 0) { - return query; + return query.OrderBy(e => e.SortName); } IOrderedQueryable<BaseItemEntity>? orderedQuery = null; @@ -1304,34 +1431,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) @@ -1346,7 +1478,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,6 +1561,7 @@ public sealed class BaseItemRepository } var includeTypes = filter.IncludeItemTypes; + // Only specify excluded types if no included types are specified if (filter.IncludeItemTypes.Length == 0) { @@ -1454,25 +1587,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) @@ -1578,7 +1696,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) @@ -1730,64 +1848,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) @@ -1796,61 +1909,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) { @@ -1945,30 +2070,30 @@ public sealed class BaseItemRepository 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; @@ -2009,14 +2134,12 @@ 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) @@ -2062,19 +2185,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(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id))); } if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) @@ -2109,7 +2232,7 @@ public sealed class BaseItemRepository { baseQuery = baseQuery .Where(e => - e.ParentAncestors! + 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}\""))); @@ -2118,7 +2241,7 @@ public sealed class BaseItemRepository 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.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)))); } } diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index 93e15735c..e0d23a261 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; -using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; @@ -31,19 +29,7 @@ public class ChapterRepository : IChapterRepository _imageProcessor = imageProcessor; } - /// <inheritdoc cref="IChapterRepository"/> - public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) - { - return GetChapter(baseItem.Id, index); - } - - /// <inheritdoc cref="IChapterRepository"/> - public IReadOnlyList<ChapterInfo> GetChapters(BaseItemDto baseItem) - { - return GetChapters(baseItem.Id); - } - - /// <inheritdoc cref="IChapterRepository"/> + /// <inheritdoc /> public ChapterInfo? GetChapter(Guid baseItemId, int index) { using var context = _dbProvider.CreateDbContext(); @@ -62,7 +48,7 @@ public class ChapterRepository : IChapterRepository return null; } - /// <inheritdoc cref="IChapterRepository"/> + /// <inheritdoc /> public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId) { using var context = _dbProvider.CreateDbContext(); @@ -77,7 +63,7 @@ public class ChapterRepository : IChapterRepository .ToArray(); } - /// <inheritdoc cref="IChapterRepository"/> + /// <inheritdoc /> public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters) { using var context = _dbProvider.CreateDbContext(); @@ -95,6 +81,14 @@ public class ChapterRepository : IChapterRepository } } + /// <inheritdoc /> + public void DeleteChapters(Guid itemId) + { + using var context = _dbProvider.CreateDbContext(); + context.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDelete(); + context.SaveChanges(); + } + private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) { return new Chapter() @@ -118,7 +112,12 @@ public class ChapterRepository : IChapterRepository ImagePath = chapterInfo.ImagePath, Name = chapterInfo.Name, }; - chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified); + + if (!string.IsNullOrEmpty(chapterInfo.ImagePath)) + { + chapterEntity.ImageTag = _imageProcessor.GetImageCacheTag(baseItemPath, chapterEntity.ImageDateModified); + } + return chapterEntity; } } diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs new file mode 100644 index 000000000..93c6f472e --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using MediaBrowser.Controller.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// <summary> +/// Repository for obtaining Keyframe data. +/// </summary> +public class KeyframeRepository : IKeyframeRepository +{ + private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; + + /// <summary> + /// Initializes a new instance of the <see cref="KeyframeRepository"/> class. + /// </summary> + /// <param name="dbProvider">The EFCore db factory.</param> + public KeyframeRepository(IDbContextFactory<JellyfinDbContext> dbProvider) + { + _dbProvider = dbProvider; + } + + private static MediaEncoding.Keyframes.KeyframeData Map(KeyframeData entity) + { + return new MediaEncoding.Keyframes.KeyframeData( + entity.TotalDuration, + (entity.KeyframeTicks ?? []).ToList()); + } + + private KeyframeData Map(MediaEncoding.Keyframes.KeyframeData dto, Guid itemId) + { + return new() + { + ItemId = itemId, + TotalDuration = dto.TotalDuration, + KeyframeTicks = dto.KeyframeTicks.ToList() + }; + } + + /// <inheritdoc /> + public IReadOnlyList<MediaEncoding.Keyframes.KeyframeData> GetKeyframeData(Guid itemId) + { + using var context = _dbProvider.CreateDbContext(); + + return context.KeyframeData.AsNoTracking().Where(e => e.ItemId.Equals(itemId)).Select(e => Map(e)).ToList(); + } + + /// <inheritdoc /> + public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken) + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + + /// <inheritdoc /> + public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken) + { + using var context = _dbProvider.CreateDbContext(); + await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 36c3b9e56..7eb13b740 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -100,7 +100,18 @@ public class MediaStreamRepository : IMediaStreamRepository dto.IsAVC = entity.IsAvc; dto.Codec = entity.Codec; - dto.Language = entity.Language; + + var language = entity.Language; + + // Check if the language has multiple three letter ISO codes + // if yes choose the first as that is the ISO 639-2/T code we're needing + if (language != null && _localization.TryGetISO6392TFromB(language, out string? isoT)) + { + language = isoT; + } + + dto.Language = language; + dto.ChannelLayout = entity.ChannelLayout; dto.Profile = entity.Profile; dto.AspectRatio = entity.AspectRatio; @@ -140,6 +151,7 @@ public class MediaStreamRepository : IMediaStreamRepository dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault(); dto.Rotation = entity.Rotation; + dto.Hdr10PlusPresentFlag = entity.Hdr10PlusPresentFlag; if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) { @@ -207,7 +219,8 @@ public class MediaStreamRepository : IMediaStreamRepository BlPresentFlag = dto.BlPresentFlag, DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId, IsHearingImpaired = dto.IsHearingImpaired, - Rotation = dto.Rotation + Rotation = dto.Rotation, + Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag, }; return entity; } diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index 03249b927..a0c127031 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -44,7 +44,7 @@ public static class OrderMapper ItemSortBy.DateCreated => e => e.DateCreated, ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.Name, + ItemSortBy.Name => e => e.CleanName, ItemSortBy.CommunityRating => e => e.CommunityRating, ItemSortBy.ProductionYear => e => e.ProductionYear, ItemSortBy.CriticRating => e => e.CriticRating, diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 77877835e..be58e2a52 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Libraries; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; @@ -53,7 +54,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery filter) { using var context = _dbProvider.CreateDbContext(); - var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter); + var dbQuery = TranslateQuery(context.Peoples.AsNoTracking(), context, filter).Select(e => e.Name).Distinct(); // dbQuery = dbQuery.OrderBy(e => e.ListOrder); if (filter.Limit > 0) @@ -61,41 +62,48 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I dbQuery = dbQuery.Take(filter.Limit); } - return dbQuery.Select(e => e.Name).ToArray(); + return dbQuery.ToArray(); } /// <inheritdoc /> public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people) { using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete(); // TODO: yes for __SOME__ reason there can be duplicates. - foreach (var item in people.DistinctBy(e => e.Id)) + people = people.DistinctBy(e => e.Id).ToArray(); + var personids = people.Select(f => f.Id); + var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray(); + context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map)); + context.SaveChanges(); + + var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList(); + foreach (var person in people) { - var personEntity = Map(item); - var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id); - if (existingEntity is null) + var existingMap = maps.FirstOrDefault(e => e.PeopleId == person.Id); + if (existingMap is null) { - context.Peoples.Add(personEntity); - existingEntity = personEntity; + context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + { + Item = null!, + ItemId = itemId, + People = null!, + PeopleId = person.Id, + ListOrder = person.SortOrder, + SortOrder = person.SortOrder, + Role = person.Role + }); } - - context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + else { - Item = null!, - ItemId = itemId, - People = existingEntity, - PeopleId = existingEntity.Id, - ListOrder = item.SortOrder, - SortOrder = item.SortOrder, - Role = item.Role - }); + // person mapping already exists so remove from list + maps.Remove(existingMap); + } } + context.PeopleBaseItemMap.RemoveRange(maps); + context.SaveChanges(); - transaction.Commit(); } private PersonInfo Map(People people) @@ -133,9 +141,13 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I if (filter.User is not null && filter.IsFavorite.HasValue) { var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]; - query = query.Where(e => e.PersonType == personType) - .Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id))) - .Select(f => f.Name).Contains(e.Name)); + var oldQuery = query; + + query = context.UserData + .Where(u => u.Item!.Type == personType && u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.User.Id)) + .Join(oldQuery, e => e.Item!.Name, e => e.Name, (item, person) => person) + .Distinct() + .AsNoTracking(); } if (!filter.ItemId.IsEmpty()) |
