From e4b82025b8cde9948671f26da05fda7915f9b0a4 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:09:53 +0100 Subject: [PATCH 1/3] move reattaching user data to own function and call it only after fetching metadata for the first time --- CONTRIBUTORS.md | 1 + .../Library/LibraryManager.cs | 6 ++++ .../Item/BaseItemRepository.cs | 32 +++++++++++-------- MediaBrowser.Controller/Entities/BaseItem.cs | 2 ++ .../Library/ILibraryManager.cs | 7 ++++ .../Persistence/IItemRepository.cs | 7 ++++ .../Manager/MetadataService.cs | 11 +++++-- 7 files changed, 49 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a1ba8f17a0..3b7d6d0a16 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -207,6 +207,7 @@ - [TokerX](https://github.com/TokerX) - [GeneMarks](https://github.com/GeneMarks) - [martenumberto](https://github.com/martenumberto) + - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) # Emby Contributors diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 83c4eb2e91..83b135f924 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync([item], parent, updateReason, cancellationToken); + /// + public void ReattachUserData(BaseItem item, CancellationToken cancellationToken) + { + _itemRepository.ReattachUserData(item, cancellationToken); + } + public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { if (item.IsFileProtocol) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 289ead11d7..f4c4cb731a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -617,7 +617,6 @@ public sealed class BaseItemRepository 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) { @@ -651,19 +650,6 @@ public sealed class BaseItemRepository 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(); @@ -759,6 +745,24 @@ public sealed class BaseItemRepository transaction.Commit(); } + /// + public void ReattachUserData(BaseItemDto item, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(item); + cancellationToken.ThrowIfCancellationRequested(); + + using var context = _dbProvider.CreateDbContext(); + + var userKeys = item.GetUserDataKeys().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.Id) + .SetProperty(f => f.RetentionDate, retentionDate)); + } + /// public BaseItemDto? RetrieveItem(Guid id) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d9d2d0e3a8..4938b43e4b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,6 +2053,8 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); + public void ReattachUserData(CancellationToken cancellationToken) => LibraryManager.ReattachUserData(this, cancellationToken); + /// /// Validates that images within the item are still on the filesystem. /// diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index fcc5ed672a..32bacc8dc2 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -281,6 +281,13 @@ namespace MediaBrowser.Controller.Library /// Returns a Task that can be awaited. Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 0026ab2b5f..9443dd3f20 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -35,6 +35,13 @@ public interface IItemRepository void SaveImages(BaseItem item); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index a2102ca9cd..5b82b18cc3 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager if (isFirstRefresh) { - await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false); } // Next run metadata providers @@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false); } return updateType; @@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager } } - protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken) { await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); + if (reattachUserData) + { + result.Item.ReattachUserData(cancellationToken); + } + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; From 09a1c31fa303856c8b9724df06f68eb5bb88ea05 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Wed, 31 Dec 2025 03:06:07 +0100 Subject: [PATCH 2/3] Refactor ReattachUserData methods to be asynchronous --- Emby.Server.Implementations/Library/LibraryManager.cs | 4 ++-- .../Item/BaseItemRepository.cs | 10 ++++++---- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++- MediaBrowser.Controller/Library/ILibraryManager.cs | 3 ++- MediaBrowser.Controller/Persistence/IItemRepository.cs | 3 ++- MediaBrowser.Providers/Manager/MetadataService.cs | 2 +- 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 83b135f924..1716c49e59 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2203,9 +2203,9 @@ namespace Emby.Server.Implementations.Library => UpdateItemsAsync([item], parent, updateReason, cancellationToken); /// - public void ReattachUserData(BaseItem item, CancellationToken cancellationToken) + public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken) { - _itemRepository.ReattachUserData(item, cancellationToken); + await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false); } public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f4c4cb731a..8191bd02e1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -746,7 +746,7 @@ public sealed class BaseItemRepository } /// - public void ReattachUserData(BaseItemDto item, CancellationToken cancellationToken) + public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(item); cancellationToken.ThrowIfCancellationRequested(); @@ -755,12 +755,14 @@ public sealed class BaseItemRepository var userKeys = item.GetUserDataKeys().ToArray(); var retentionDate = (DateTime?)null; - context.UserData + await context.UserData .Where(e => e.ItemId == PlaceholderId) .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdate(e => e + .ExecuteUpdateAsync( + e => e .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate)); + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4938b43e4b..7586b99e77 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,7 +2053,8 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); - public void ReattachUserData(CancellationToken cancellationToken) => LibraryManager.ReattachUserData(this, cancellationToken); + public async Task ReattachUserDataAsync(CancellationToken cancellationToken) => + await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false); /// /// Validates that images within the item are still on the filesystem. diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 32bacc8dc2..675812ac23 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -286,7 +286,8 @@ namespace MediaBrowser.Controller.Library /// /// The item. /// The cancellation token. - void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); /// /// Retrieves the item. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 9443dd3f20..790efb86a6 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -40,7 +40,8 @@ public interface IItemRepository /// /// The item. /// The cancellation token. - void ReattachUserData(BaseItem item, CancellationToken cancellationToken); + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); /// /// Retrieves the item. diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 5b82b18cc3..e9cb46eab5 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -280,7 +280,7 @@ namespace MediaBrowser.Providers.Manager await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); if (reattachUserData) { - result.Item.ReattachUserData(cancellationToken); + await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); } if (result.Item.SupportsPeople && result.People is not null) From adaca955901ec2b332dae1cdfa58c79c2ef754b4 Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Wed, 31 Dec 2025 07:43:07 +0100 Subject: [PATCH 3/3] make db context creation async --- .../Item/BaseItemRepository.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 8191bd02e1..5d26393111 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -751,18 +751,21 @@ public sealed class BaseItemRepository ArgumentNullException.ThrowIfNull(item); cancellationToken.ThrowIfCancellationRequested(); - using var context = _dbProvider.CreateDbContext(); + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - var userKeys = item.GetUserDataKeys().ToArray(); - var retentionDate = (DateTime?)null; - await context.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdateAsync( - e => e - .SetProperty(f => f.ItemId, item.Id) - .SetProperty(f => f.RetentionDate, retentionDate), - cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + } } ///