aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs')
-rw-r--r--Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs142
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 })