Apply review suggestions

This commit is contained in:
Shadowghost
2026-04-26 22:30:20 +02:00
parent 755ef1f942
commit 070e2b2d0c
4 changed files with 53 additions and 39 deletions

View File

@@ -87,7 +87,7 @@ public sealed partial class BaseItemRepository
return Array.Empty<BaseItemDto>();
}
var itemsById = ApplyNavigations(context.BaseItems.AsNoTracking().Where(e => orderedIds.Contains(e.Id)), filter)
var itemsById = ApplyNavigations(context.BaseItems.AsNoTracking().WhereOneOrMany(orderedIds, e => e.Id), filter)
.AsSplitQuery()
.AsEnumerable()
.Select(w => DeserializeBaseItem(w, filter.SkipDeserialization))
@@ -142,48 +142,27 @@ public sealed partial class BaseItemRepository
groupKeySelector = e => e.Album;
}
var topGroupKeys = baseQuery
// Group by GroupKey, pick the latest item per group (correlated subquery: ORDER BY DateCreated DESC, Id DESC LIMIT 1),
// order groups by group max date, take the top N — all in a single SQL statement.
// ThenByDescending(Id) is the tiebreaker for deterministic ordering when items share a DateCreated.
var topGroupItems = baseQuery
.Where(groupKeyFilter)
.GroupBy(groupKeySelector)
.Select(g => new { GroupKey = g.Key!, MaxDate = g.Max(e => e.DateCreated) })
.Select(g => new
{
MaxDate = g.Max(e => e.DateCreated),
FirstId = g.OrderByDescending(e => e.DateCreated).ThenByDescending(e => e.Id).Select(e => e.Id).First()
})
.OrderByDescending(g => g.MaxDate);
if (filter.Limit.HasValue)
{
topGroupKeys = topGroupKeys.Take(filter.Limit.Value).OrderByDescending(g => g.MaxDate);
}
var firstIdsQuery = filter.Limit.HasValue
? topGroupItems.Take(filter.Limit.Value).Select(g => g.FirstId)
: topGroupItems.Select(g => g.FirstId);
// Get only the first (most recent) item ID per group using a lightweight projection,
// then fetch full entities only for those items. This avoids loading all versions/tracks
// with expensive navigation properties just to discard duplicates.
var topGroupKeyList = topGroupKeys.Select(g => g.GroupKey).ToList();
// ThenByDescending(Id) is a tiebreaker for deterministic ordering when multiple items
// share the same DateCreated timestamp — without it, SQL returns arbitrary order across queries.
var allItemsLite = collectionType switch
{
CollectionType.movies => baseQuery
.Where(e => e.PresentationUniqueKey != null && topGroupKeyList.Contains(e.PresentationUniqueKey))
.OrderByDescending(e => e.DateCreated)
.ThenByDescending(e => e.Id)
.Select(e => new { e.Id, GroupKey = e.PresentationUniqueKey })
.AsEnumerable(),
_ => baseQuery
.Where(e => e.Album != null && topGroupKeyList.Contains(e.Album))
.OrderByDescending(e => e.DateCreated)
.ThenByDescending(e => e.Id)
.Select(e => new { e.Id, GroupKey = e.Album })
.AsEnumerable()
};
var firstIds = firstIdsQuery.ToList();
// Client-side DistinctBy: EF Core/SQLite cannot reliably translate
// GroupBy(...).Select(g => g.First()) to SQL. The projection is lightweight
// (only Id + GroupKey for ~50 items), so client-side dedup is negligible.
var firstIds = allItemsLite
.DistinctBy(e => e.GroupKey)
.Select(e => e.Id)
.ToList();
var itemsQuery = context.BaseItems.AsNoTracking().Where(e => firstIds.Contains(e.Id));
// Single bound JSON / array parameter via WhereOneOrMany — keeps SQL small regardless of N.
var itemsQuery = context.BaseItems.AsNoTracking().WhereOneOrMany(firstIds, e => e.Id);
itemsQuery = ApplyNavigations(itemsQuery, filter);
return itemsQuery
@@ -250,13 +229,14 @@ public sealed partial class BaseItemRepository
? topSeriesData.Min(g => g.MaxDate)?.AddHours(-RecentAdditionWindowHours)
: null;
// Fetch only the columns needed for analysis (lightweight projection).
// Restrict to episodes of the top series, optionally bounded by the global cutoff.
var episodeQuery = baseQuery.Where(e => e.SeriesName != null && topSeriesNames.Contains(e.SeriesName));
if (globalCutoff is not null)
{
episodeQuery = episodeQuery.Where(e => e.DateCreated >= globalCutoff);
}
// Lightweight projection: only the columns needed for the in-memory analysis below.
var allEpisodes = episodeQuery
.OrderByDescending(e => e.DateCreated)
.ThenByDescending(e => e.Id)

View File

@@ -54,7 +54,7 @@ public class LinkedChildrenService : ILinkedChildrenService
return query
.OrderBy(lc => lc.SortOrder)
.Select(lc => lc.ChildId)
.ToList();
.ToArray();
}
/// <inheritdoc/>

View File

@@ -87,10 +87,19 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
// Collect all duplicate IDs to delete in one batch
var allIdsToDelete = new List<Guid>();
const int progressLogStep = 500;
var processedPaths = 0;
foreach (var path in duplicatePaths)
{
cancellationToken.ThrowIfCancellationRequested();
if (processedPaths > 0 && processedPaths % progressLogStep == 0)
{
_logger.LogInformation("Resolving duplicates: {Processed}/{Total} paths", processedPaths, duplicatePaths.Count);
}
processedPaths++;
// Get all items with this path
var itemsWithPath = await context.BaseItems
.Where(b => b.Path == path)
@@ -208,6 +217,7 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
}
_logger.LogInformation("Found {Count} orphaned extras to reassign", orphanedExtras.Count);
const int extraProgressLogStep = 500;
// Build a lookup of directory -> first video/movie item for parent resolution
var extraDirectories = orphanedExtras
@@ -244,8 +254,16 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
}
var reassignedCount = 0;
var processedExtras = 0;
foreach (var extra in orphanedExtras)
{
if (processedExtras > 0 && processedExtras % extraProgressLogStep == 0)
{
_logger.LogInformation("Reassigning orphaned extras: {Processed}/{Total}", processedExtras, orphanedExtras.Count);
}
processedExtras++;
if (string.IsNullOrEmpty(extra.Path))
{
continue;
@@ -299,8 +317,17 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine
.ConfigureAwait(false);
var updatedCount = 0;
const int linkProgressLogStep = 1000;
var processedLinks = 0;
foreach (var link in alternateVersionLinks)
{
if (processedLinks > 0 && processedLinks % linkProgressLogStep == 0)
{
_logger.LogInformation("Populating PrimaryVersionId: {Processed}/{Total} links", processedLinks, alternateVersionLinks.Count);
}
processedLinks++;
if (childItems.TryGetValue(link.ChildId, out var childItem))
{
childItem.PrimaryVersionId = link.ParentId;

View File

@@ -75,6 +75,8 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
var linkedChildrenToAdd = new List<LinkedChildEntity>();
var processedCount = 0;
const int progressLogStep = 1000;
var totalItems = itemsWithData.Count;
foreach (var item in itemsWithData)
{
@@ -83,6 +85,11 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
continue;
}
if (processedCount > 0 && processedCount % progressLogStep == 0)
{
_logger.LogInformation("Processing LinkedChildren: {Processed}/{Total} items", processedCount, totalItems);
}
try
{
using var doc = JsonDocument.Parse(item.Data);