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);
+ }
}
///