737c423d9c
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.
150 lines
5.9 KiB
C#
150 lines
5.9 KiB
C#
using System.Linq.Expressions;
|
|
using DeepDrftData.Repositories;
|
|
using DeepDrftModels.DTOs;
|
|
using DeepDrftModels.Entities;
|
|
using DeepDrftModels.Enums;
|
|
using Microsoft.Extensions.Logging;
|
|
using Models.Common;
|
|
using NetBlocks.Models;
|
|
|
|
namespace DeepDrftData;
|
|
|
|
/// <summary>
|
|
/// SQL-side release service implementing <see cref="IReleaseService"/>. Deliberately does NOT extend
|
|
/// <c>Manager<></c>: that CRUD base does not fit this read-projection + satellite-write purpose.
|
|
/// The layer boundary holds — ReleaseRepository outputs entities, this manager outputs DTOs via
|
|
/// TrackConverter, the single authoritative conversion path.
|
|
/// </summary>
|
|
public class ReleaseManager : IReleaseService
|
|
{
|
|
// Distinguishes "release does not exist" from a real failure so the controller can map to 404.
|
|
public const string ReleaseNotFoundMessage = "Release not found.";
|
|
|
|
private readonly ReleaseRepository _repository;
|
|
private readonly ILogger<ReleaseManager> _logger;
|
|
|
|
public ReleaseManager(ReleaseRepository repository, ILogger<ReleaseManager> logger)
|
|
{
|
|
_repository = repository;
|
|
_logger = logger;
|
|
}
|
|
|
|
// Nulls sort to end via the coalescing sentinels, matching TrackManager's convention.
|
|
private static Expression<Func<ReleaseEntity, object>> GetOrderExpression(string? sortColumn)
|
|
=> sortColumn switch
|
|
{
|
|
"Title" => r => r.Title,
|
|
"Artist" => r => r.Artist,
|
|
"ReleaseDate" => r => (object)(r.ReleaseDate ?? DateOnly.MaxValue),
|
|
"Medium" => r => r.Medium,
|
|
_ => r => r.Id
|
|
};
|
|
|
|
public async Task<ResultContainer<PagedResult<ReleaseDto>>> GetPagedAsync(
|
|
int page, int pageSize, string? sortColumn, bool sortDescending,
|
|
ReleaseMedium? medium, ReleaseFilter? filter = null, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var parameters = new PagingParameters<ReleaseEntity>
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
OrderBy = GetOrderExpression(sortColumn),
|
|
IsDescending = sortDescending,
|
|
};
|
|
|
|
// 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);
|
|
|
|
var dtos = entityPage.Items
|
|
.Select(r =>
|
|
{
|
|
var dto = TrackConverter.Convert(r);
|
|
dto.TrackCount = counts.GetValueOrDefault(r.Id);
|
|
return dto;
|
|
});
|
|
|
|
var dtoPage = PagedResult<ReleaseDto>.From(entityPage, dtos);
|
|
return ResultContainer<PagedResult<ReleaseDto>>.CreatePassResult(dtoPage);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return ResultContainer<PagedResult<ReleaseDto>>.CreateFailResult(e.Message);
|
|
}
|
|
}
|
|
|
|
public async Task<ResultContainer<ReleaseDto?>> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var entity = await _repository.GetByIdWithMetadataAsync(id, cancellationToken);
|
|
// TrackConverter nulls the non-matching satellite. TrackCount is not loaded for the detail
|
|
// read (the Tracks collection isn't Include'd) and is not needed by detail consumers.
|
|
return ResultContainer<ReleaseDto?>.CreatePassResult(
|
|
entity is null ? null : TrackConverter.Convert(entity));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return ResultContainer<ReleaseDto?>.CreateFailResult(e.Message);
|
|
}
|
|
}
|
|
|
|
public async Task<ResultContainer<List<string>>> GetTrackEntryKeysAsync(long releaseId, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var keys = await _repository.GetTrackEntryKeysByReleaseIdAsync(releaseId, cancellationToken);
|
|
return ResultContainer<List<string>>.CreatePassResult(keys);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return ResultContainer<List<string>>.CreateFailResult(e.Message);
|
|
}
|
|
}
|
|
|
|
public async Task<Result> SetSessionHeroImageAsync(long releaseId, string heroImageEntryKey, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var release = await _repository.GetByIdWithMetadataAsync(releaseId, cancellationToken);
|
|
if (release is null)
|
|
return Result.CreateFailResult(ReleaseNotFoundMessage);
|
|
|
|
if (release.Medium != ReleaseMedium.Session)
|
|
return Result.CreateFailResult($"Release {releaseId} is not a Session medium.");
|
|
|
|
await _repository.SetHeroImageEntryKeyAsync(releaseId, heroImageEntryKey, cancellationToken);
|
|
return Result.CreatePassResult();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return Result.CreateFailResult(e.Message);
|
|
}
|
|
}
|
|
|
|
public async Task<Result> SetMixWaveformAsync(long releaseId, string waveformEntryKey, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
var release = await _repository.GetByIdWithMetadataAsync(releaseId, cancellationToken);
|
|
if (release is null)
|
|
return Result.CreateFailResult(ReleaseNotFoundMessage);
|
|
|
|
if (release.Medium != ReleaseMedium.Mix)
|
|
return Result.CreateFailResult($"Release {releaseId} is not a Mix medium.");
|
|
|
|
await _repository.SetWaveformEntryKeyAsync(releaseId, waveformEntryKey, cancellationToken);
|
|
return Result.CreatePassResult();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return Result.CreateFailResult(e.Message);
|
|
}
|
|
}
|
|
}
|