aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
authorShadowghost <Shadowghost@users.noreply.github.com>2026-10-05 19:18:41 -0400
committerCody Robibero <cody@robibe.ro>2026-10-05 19:18:41 -0400
commite7764cbbd9db5e20ce764121d550b732405100e3 (patch)
tree6a88640e294ef6499ee26eadadbce2c2551b0eab /Jellyfin.Server.Implementations
parent2e5e042f22f30fa7f49339f6a7f51cbec32383c5 (diff)
Backport pull request #18001 from jellyfin/release-12.z
Keep an item's user data rows in agreement Original-merge: fc98019b12e311fea244dccef72d521278326024 Merged-by: crobibero <cody@robibe.ro> Backported-by: Cody Robibero <cody@robibe.ro>
Diffstat (limited to 'Jellyfin.Server.Implementations')
-rw-r--r--Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs73
1 files changed, 65 insertions, 8 deletions
diff --git a/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs b/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs
index 8b79e44680..3bf33b969e 100644
--- a/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs
+++ b/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs
@@ -230,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()
@@ -253,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);