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:
@@ -12,10 +12,10 @@ namespace DeepDrftData;
|
||||
/// </summary>
|
||||
public interface IReleaseService
|
||||
{
|
||||
/// <summary>Paginated releases, optionally filtered to one medium. The matching medium's metadata satellite is included in the result. Omit medium for all releases.</summary>
|
||||
/// <summary>Paginated releases, optionally narrowed by medium and a free-text/genre filter. The matching medium's metadata satellite is included in the result. Omit medium for all releases; omit filter for no search/genre narrowing.</summary>
|
||||
Task<ResultContainer<PagedResult<ReleaseDto>>> GetPagedAsync(
|
||||
int page, int pageSize, string? sortColumn, bool sortDescending,
|
||||
ReleaseMedium? medium, CancellationToken cancellationToken = default);
|
||||
ReleaseMedium? medium, ReleaseFilter? filter = null, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Single release with both metadata navs included (nulls for non-matching media).</summary>
|
||||
Task<ResultContainer<ReleaseDto?>> GetByIdAsync(long id, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -42,7 +42,7 @@ public class ReleaseManager : IReleaseService
|
||||
|
||||
public async Task<ResultContainer<PagedResult<ReleaseDto>>> GetPagedAsync(
|
||||
int page, int pageSize, string? sortColumn, bool sortDescending,
|
||||
ReleaseMedium? medium, CancellationToken cancellationToken = default)
|
||||
ReleaseMedium? medium, ReleaseFilter? filter = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -54,7 +54,9 @@ public class ReleaseManager : IReleaseService
|
||||
IsDescending = sortDescending,
|
||||
};
|
||||
|
||||
var entityPage = await _repository.GetPagedByMediumAsync(parameters, medium, cancellationToken);
|
||||
// Collapse an all-null filter to null so the repository skips the predicate block entirely.
|
||||
var effectiveFilter = filter is { IsEmpty: false } ? filter : null;
|
||||
var entityPage = await _repository.GetPagedByMediumAsync(parameters, medium, effectiveFilter, cancellationToken);
|
||||
|
||||
var releaseIds = entityPage.Items.Select(r => r.Id).ToList();
|
||||
var counts = await _repository.GetTrackCountsByReleaseIdsAsync(releaseIds, cancellationToken);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user