Complete LinkedChildren integration and batch DTO optimizations

This commit integrates remaining performance changes:

- Add batch user data fetching in DtoService to reduce N+1 queries
- Add GetNextUpEpisodesBatch in TVSeriesManager for efficient batch retrieval
- Update Video/Movie/BoxSet to use LibraryManager for alternate versions
- Transition LinkedChild to use ItemId instead of Path (obsolete Path/LibraryItemId)
- Update providers and controllers for LinkedChildren-based references
- Add NextUpEpisodeBatchResult for batched episode queries
- Integrate IDescendantQueryProvider in SqliteDatabaseProvider
This commit is contained in:
Shadowghost
2026-01-17 17:10:07 +01:00
parent dfa78590c2
commit 5996c4afce
35 changed files with 2277 additions and 936 deletions

View File

@@ -406,6 +406,37 @@ namespace Emby.Server.Implementations.Library
item.Id);
}
// If deleting a primary version video, clear PrimaryVersionId from alternate versions
if (item is Video video && string.IsNullOrEmpty(video.PrimaryVersionId))
{
var alternateVersions = GetLocalAlternateVersionIds(video)
.Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
.Distinct()
.Select(id => GetItemById(id))
.OfType<Video>()
.ToList();
if (alternateVersions.Count > 0)
{
_logger.LogInformation(
"Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
alternateVersions.Count,
item.Name ?? "Unknown name");
// Promote the first alternate version to be the new primary
var newPrimary = alternateVersions[0];
newPrimary.SetPrimaryVersionId(null);
newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
// Update remaining alternates to point to new primary
foreach (var alternate in alternateVersions.Skip(1))
{
alternate.SetPrimaryVersionId(newPrimary.Id.ToString("N", CultureInfo.InvariantCulture));
alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
}
}
}
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
: [];
@@ -576,6 +607,9 @@ namespace Emby.Server.Implementations.Library
// Trickplay
list.Add(_pathManager.GetTrickplayDirectory(video));
// Chapter Images
list.Add(_pathManager.GetChapterImageFolderPath(video));
// Subtitles and attachments
foreach (var mediaSource in item.GetMediaSources(false))
{
@@ -1421,14 +1455,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent);
}
var itemList = _itemRepository.GetItemList(query);
var user = query.User;
if (user is not null)
{
return itemList.Where(i => i.IsVisible(user)).ToList();
}
return itemList;
return _itemRepository.GetItemList(query);
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1474,6 +1501,11 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemCounts(query);
}
public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
{
return _itemRepository.GetChildCountBatch(parentIds, userId);
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -1519,6 +1551,16 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
}
/// <inheritdoc />
public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisodesBatch(
InternalItemsQuery query,
IReadOnlyList<string> seriesKeys,
bool includeSpecials,
bool includeWatchedForRewatching)
{
return _itemRepository.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching);
}
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
{
if (query.User is not null)
@@ -1700,6 +1742,11 @@ namespace Emby.Server.Implementations.Library
private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
{
if (query.User is null)
{
query.SetUser(user);
}
if (query.AncestorIds.Length == 0 &&
query.ParentId.IsEmpty() &&
query.ChannelIds.Count == 0 &&
@@ -1725,6 +1772,15 @@ namespace Emby.Server.Implementations.Library
}
}
/// <inheritdoc/>
public void ConfigureUserAccess(InternalItemsQuery query, User user)
{
ArgumentNullException.ThrowIfNull(query);
ArgumentNullException.ThrowIfNull(user);
AddUserToQuery(query, user);
}
private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
{
if (item is UserView view)
@@ -1889,6 +1945,38 @@ namespace Emby.Server.Implementations.Library
return video;
}
/// <inheritdoc />
public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
{
ArgumentNullException.ThrowIfNull(video);
var linkedIds = _itemRepository.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LocalAlternateVersion);
if (linkedIds.Count > 0)
{
return linkedIds;
}
return [];
}
/// <inheritdoc />
public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
{
ArgumentNullException.ThrowIfNull(video);
var linkedIds = _itemRepository.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.LinkedChildType.LinkedAlternateVersion);
if (linkedIds.Count > 0)
{
return linkedIds
.Select(id => GetItemById(id))
.Where(i => i is not null)
.OfType<Video>()
.OrderBy(i => i.SortName);
}
return [];
}
/// <inheritdoc />
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
{
@@ -2896,10 +2984,17 @@ namespace Emby.Server.Implementations.Library
extra.ExtraType = extraType;
}
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
extra.IsInMixedFolder = isInMixedFolder;
return extra;
// Only set OwnerId if this is actually an extra (not Unknown or null)
if (extra.ExtraType is not null)
{
extra.ParentId = Guid.Empty;
extra.OwnerId = owner.Id;
extra.IsInMixedFolder = isInMixedFolder;
return extra;
}
return null;
}
}