diff options
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs')
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs | 142 |
1 files changed, 120 insertions, 22 deletions
diff --git a/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs b/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs index c8672e189b..3bf33b969e 100644 --- a/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs +++ b/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -68,16 +69,24 @@ public class ItemPersistenceService : IItemPersistenceService // Use WhereOneOrMany instead of a raw HashSet.Contains so large id sets are bound as a // single parameter (json_each) rather than one SQL variable per id, which would otherwise // overflow SQLite's variable limit when deleting many items at once (e.g. migrations). - var ownerIds = descendantIds.ToArray(); - var extraIds = context.BaseItems - .Where(e => e.OwnerId.HasValue) - .WhereOneOrMany(ownerIds, e => e.OwnerId!.Value) - .Select(e => e.Id) - .ToArray(); - - foreach (var extraId in extraIds) + var frontier = descendantIds.ToArray(); + while (frontier.Length > 0) { - descendantIds.Add(extraId); + var ownedIds = context.BaseItems + .Where(e => e.OwnerId.HasValue) + .WhereOneOrMany(frontier, e => e.OwnerId!.Value) + .Select(e => e.Id) + .ToArray(); + + var childIds = context.BaseItems + .Where(e => e.ParentId.HasValue) + .WhereOneOrMany(frontier, e => e.ParentId!.Value) + .Select(e => e.Id) + .ToArray(); + + // Only ids that were not already known become the next frontier, so ownership cycles + // terminate instead of looping forever. + frontier = [.. ownedIds.Concat(childIds).Where(e => descendantIds.Add(e))]; } var relatedItems = descendantIds.ToArray(); @@ -132,17 +141,17 @@ public class ItemPersistenceService : IItemPersistenceService context.Chapters.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); context.CustomItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); context.ItemDisplayPreferences.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); - context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); context.ItemValuesMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); context.LinkedChildren.WhereOneOrMany(relatedItems, e => e.ParentId).ExecuteDelete(); context.LinkedChildren.WhereOneOrMany(relatedItems, e => e.ChildId).ExecuteDelete(); + var peopleIds = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray(); context.BaseItems.WhereOneOrMany(relatedItems, e => e.Id).ExecuteDelete(); + context.ItemValues.Where(e => !e.BaseItemsMap!.Any()).ExecuteDelete(); context.KeyframeData.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); context.MediaSegments.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); context.MediaStreamInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); - var query = context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).Select(f => f.PeopleId).Distinct().ToArray(); context.PeopleBaseItemMap.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); - context.Peoples.WhereOneOrMany(query, e => e.Id).Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); + context.Peoples.WhereOneOrMany(peopleIds, e => e.Id).Where(e => !e.BaseItems!.Any()).ExecuteDelete(); context.TrickplayInfos.WhereOneOrMany(relatedItems, e => e.ItemId).ExecuteDelete(); context.SaveChanges(); transaction.Commit(); @@ -221,17 +230,18 @@ public class ItemPersistenceService : IItemPersistenceService var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); await using (transaction.ConfigureAwait(false)) { - var userKeys = item.GetUserDataKeys().ToArray(); - var retentionDate = (DateTime?)null; + var userKeys = item.GetUserDataKeys().Distinct().ToList(); - await dbContext.UserData + var detached = await dbContext.UserData .Where(e => e.ItemId == BaseItemRepository.PlaceholderId) .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdateAsync( - e => e - .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate), - cancellationToken).ConfigureAwait(false); + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + + if (detached.Count > 0) + { + await ReconcileUserDataAsync(dbContext, item, userKeys, detached, cancellationToken).ConfigureAwait(false); + } item.UserData = await dbContext.UserData .AsNoTracking() @@ -244,6 +254,62 @@ public class ItemPersistenceService : IItemPersistenceService } } + private static async Task ReconcileUserDataAsync( + JellyfinDbContext dbContext, + BaseItemDto item, + IReadOnlyList<string> userKeys, + List<UserData> detached, + CancellationToken cancellationToken) + { + var existing = await dbContext.UserData + .Where(e => e.ItemId == item.Id) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + + // Keys the item no longer reports are carried over: they may only be missing mid-refresh. + var winners = detached.Concat(existing) + .GroupBy(e => e.UserId) + .Select(g => ( + Winner: g + .OrderByDescending(e => e.LastPlayedDate) + .ThenByDescending(e => e.PlayCount) + .ThenByDescending(e => e.PlaybackPositionTicks) + .First(), + Keys: userKeys.Union(g.Select(e => e.CustomDataKey)).ToList())) + .ToList(); + + dbContext.UserData.RemoveRange(detached); + dbContext.UserData.RemoveRange(existing); + await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + foreach (var (winner, keys) in winners) + { + foreach (var key in keys) + { + dbContext.UserData.Add(new UserData + { + ItemId = item.Id, + Item = null, + UserId = winner.UserId, + User = null, + CustomDataKey = key, + RetentionDate = null, + AudioStreamIndex = winner.AudioStreamIndex, + IsFavorite = winner.IsFavorite, + LastPlayedDate = winner.LastPlayedDate, + Likes = winner.Likes, + PlaybackPositionTicks = winner.PlaybackPositionTicks, + PlayCount = winner.PlayCount, + Played = winner.Played, + Rating = winner.Rating, + SubtitleStreamIndex = winner.SubtitleStreamIndex + }); + } + } + + await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + private void UpdateOrInsertItems(IReadOnlyList<BaseItemDto> items, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(items); @@ -268,7 +334,7 @@ public class ItemPersistenceService : IItemPersistenceService 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).ToHashSet(); + var existingItems = context.BaseItems.WhereOneOrMany(ids, e => e.Id).Select(f => f.Id).ToHashSet(); foreach (var item in tuples) { @@ -328,7 +394,7 @@ public class ItemPersistenceService : IItemPersistenceService .Select(f => (f.Item, Values: f.Values.Select(e => itemValuesStore[(e.MagicNumber, e.Value)]).DistinctBy(e => e.ItemValueId).ToArray())) .ToArray(); - var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList(); + var mappedValues = context.ItemValuesMap.WhereOneOrMany(ids, e => e.ItemId).ToList(); foreach (var item in valueMap) { @@ -657,6 +723,38 @@ public class ItemPersistenceService : IItemPersistenceService sortOrder++; } + var linkedChildIds = newLinkedChildren + .Select(c => c.ChildId) + // A video listed among its own versions would be pointed at itself. + .Where(childId => existingChildIds.Contains(childId) && !childId.Equals(video.Id)) + .Where(childId => !childId.Equals(video.PrimaryVersionId)) + .ToList(); + if (linkedChildIds.Count > 0) + { + var demotedChildren = context.BaseItems + .Where(e => linkedChildIds.Contains(e.Id) + && (e.PrimaryVersionId == null || e.PrimaryVersionId != video.Id)) + .ToList(); + + foreach (var child in demotedChildren) + { + child.PrimaryVersionId = video.Id; + + // Mirrors Video.CreatePresentationUniqueKey, so presentation-key grouping + // collapses the version onto its primary as well. + child.PresentationUniqueKey = video.Id.ToString("N", CultureInfo.InvariantCulture); + } + + if (demotedChildren.Count > 0) + { + _logger.LogInformation( + "Set PrimaryVersionId on {Count} alternate versions of video {VideoName} ({VideoId})", + demotedChildren.Count, + video.Name, + video.Id); + } + } + // A previously-linked LocalAlternateVersion that is no longer present becomes orphaned; var previousLinkedChildren = allLinkedChildrenByParent.GetValueOrDefault(video.Id); if (previousLinkedChildren is { Count: > 0 }) |
