feat: replace /archive with release-cardinal searchable browser (Phase 9 §8.H)

Retire the three-card overview for a search + medium + genre browser over all
releases. Adds q/genre filter params to the api/release paged read path,
mirroring the existing api/track/page TrackFilter pattern.
This commit is contained in:
daniel-c-harvey
2026-06-13 20:47:50 -04:00
parent 18f4b596f2
commit 737c423d9c
13 changed files with 607 additions and 59 deletions
+23 -2
View File
@@ -1,4 +1,5 @@
using DeepDrftData.Data;
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using DeepDrftModels.Enums;
using Microsoft.EntityFrameworkCore;
@@ -36,17 +37,37 @@ public class ReleaseRepository
_ => query
};
// Paged, optionally medium-filtered release list. The matching medium's satellite is Include'd;
// total count reflects the medium filter (applied before Skip/Take).
// Paged release list, optionally narrowed by medium and a free-text/genre filter. The matching
// medium's satellite is Include'd; total count reflects every applied predicate (all before
// Skip/Take). The filter predicates mirror TrackRepository.GetPagedFilteredAsync so the release
// browse path searches and filters identically to the track path.
public async Task<PagedResult<ReleaseEntity>> GetPagedByMediumAsync(
PagingParameters<ReleaseEntity> paging,
ReleaseMedium? medium,
ReleaseFilter? filter,
CancellationToken ct)
{
IQueryable<ReleaseEntity> query = _context.Releases.Where(r => !r.IsDeleted);
if (medium.HasValue)
query = query.Where(r => r.Medium == medium.Value);
if (filter is not null)
{
if (!string.IsNullOrWhiteSpace(filter.SearchText))
{
// Postgres case-insensitive LIKE. The '%' wraps make it a contains-match; ILike is
// EF-translatable where ToLower().Contains() is not. Title/Artist are non-null columns
// on the release itself, so no navigation guard is needed (unlike the track path).
var pattern = $"%{filter.SearchText}%";
query = query.Where(r =>
EF.Functions.ILike(r.Title, pattern)
|| EF.Functions.ILike(r.Artist, pattern));
}
if (!string.IsNullOrWhiteSpace(filter.Genre))
query = query.Where(r => r.Genre == filter.Genre);
}
query = ApplyMediumInclude(query, medium);
var totalCount = await query.CountAsync(ct);