using ErsatzTV.Application.LibraryBrowse; using ErsatzTV.Core; using ErsatzTV.Core.Api.LibraryBrowse; using ErsatzTV.Core.Errors; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; public class GetCollectionItemsHandler(IDbContextFactory dbContextFactory) : IRequestHandler> { public async Task> Handle( GetCollectionItems request, CancellationToken cancellationToken) { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); bool exists = await dbContext.Collections .AsNoTracking() .AnyAsync(c => c.Id == request.Id, cancellationToken); if (!exists) { return new NotFoundError($"Collection {request.Id} does not exist."); } // The collection graph is bounded, so load every member id and hydrate them in one shared // pass (LibraryBrowseItemMapper), then order + page in-memory. Mixed media kinds are supported // because MediaItem ids are globally unique across kinds. List mediaItemIds = await dbContext.CollectionItems .AsNoTracking() .Where(ci => ci.CollectionId == request.Id) .Select(ci => ci.MediaItemId) .ToListAsync(cancellationToken); List all = await LibraryBrowseItemMapper.HydrateMediaItemsByIds(dbContext, mediaItemIds, cancellationToken); // Stable title ordering mirrors the library-browse handler (which orders its rows by name), // giving the SPA a deterministic, browsable list independent of collection insertion order. List ordered = all .OrderBy(i => i.Title, StringComparer.OrdinalIgnoreCase) .ThenBy(i => i.Id) .ToList(); int pageNum = Math.Max(0, request.PageNum); int pageSize = Math.Clamp(request.PageSize, 1, 100); List page = ordered .Skip(pageNum * pageSize) .Take(pageSize) .ToList(); return new PagedLibraryBrowseItemsResponseModel(ordered.Count, page); } }