accf20ba57
Per-track high-res datum keyed by EntryKey in the renamed track-waveforms vault; computed at upload for all tracks, regenerable per-track via CMS, with a re-runnable backfill. Mix read path repointed so it keeps working.
186 lines
8.1 KiB
C#
186 lines
8.1 KiB
C#
using DeepDrftAPI.Middleware;
|
|
using DeepDrftAPI.Services;
|
|
using DeepDrftContent.Constants;
|
|
using DeepDrftContent.FileDatabase.Models;
|
|
using DeepDrftContent.Processors;
|
|
using DeepDrftData;
|
|
using DeepDrftModels.DTOs;
|
|
using DeepDrftModels.Enums;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace DeepDrftAPI.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class ReleaseController : ControllerBase
|
|
{
|
|
private readonly IReleaseService _releaseService;
|
|
private readonly UnifiedReleaseService _unifiedReleaseService;
|
|
private readonly WaveformProfileService _waveformProfileService;
|
|
private readonly ILogger<ReleaseController> _logger;
|
|
|
|
public ReleaseController(
|
|
IReleaseService releaseService,
|
|
UnifiedReleaseService unifiedReleaseService,
|
|
WaveformProfileService waveformProfileService,
|
|
ILogger<ReleaseController> logger)
|
|
{
|
|
_releaseService = releaseService;
|
|
_unifiedReleaseService = unifiedReleaseService;
|
|
_waveformProfileService = waveformProfileService;
|
|
_logger = logger;
|
|
}
|
|
|
|
// GET api/release?medium=session&q=text&genre=House&page=1&pageSize=20&sortColumn=Title&sortDescending=false (unauth)
|
|
// Paged release list, optionally narrowed by medium, free-text search (q), and genre. The matching
|
|
// medium's metadata satellite is populated; the others are null. Backs the public /archive browser.
|
|
// Public browse data, same auth posture as GET api/track/page.
|
|
[HttpGet]
|
|
public async Task<ActionResult> GetReleases(
|
|
[FromQuery] string? medium = null,
|
|
[FromQuery] string? q = null,
|
|
[FromQuery] string? genre = null,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20,
|
|
[FromQuery] string? sortColumn = null,
|
|
[FromQuery] bool sortDescending = false,
|
|
CancellationToken ct = default)
|
|
{
|
|
ReleaseMedium? parsedMedium = null;
|
|
if (!string.IsNullOrWhiteSpace(medium))
|
|
{
|
|
if (!Enum.TryParse<ReleaseMedium>(medium, ignoreCase: true, out var m) || !Enum.IsDefined(m))
|
|
return BadRequest($"Unrecognised medium: {medium}");
|
|
parsedMedium = m;
|
|
}
|
|
|
|
var filter = new ReleaseFilter { SearchText = q, Genre = genre };
|
|
var result = await _releaseService.GetPagedAsync(page, pageSize, sortColumn, sortDescending, parsedMedium, filter, ct);
|
|
if (!result.Success || result.Value is null)
|
|
{
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
_logger.LogError("GetReleases failed: {Error}", error);
|
|
return StatusCode(500, "Failed to load releases");
|
|
}
|
|
|
|
return Ok(result.Value);
|
|
}
|
|
|
|
// GET api/release/{entryKey}/mix/waveform (unauthenticated)
|
|
// Serves the high-res waveform datum for a Mix release as base64. Mirrors GET api/track/{id}/waveform
|
|
// but reads the Mix's track datum from the track-waveforms vault. 404 when the release is not a Mix,
|
|
// carries no waveform key,
|
|
// or no datum is stored. Public read — addresses by the opaque EntryKey, not the int PK (§3e). The
|
|
// {entryKey} string segment cannot collide with the ApiKey-gated POST {id:long}/mix/waveform (different
|
|
// verb + constraint). Declared before the shorter "{entryKey}" route for clarity.
|
|
[HttpGet("{entryKey}/mix/waveform")]
|
|
public async Task<ActionResult> GetMixWaveform(string entryKey, CancellationToken ct = default)
|
|
{
|
|
var lookup = await _releaseService.GetByEntryKeyAsync(entryKey, ct);
|
|
if (!lookup.Success)
|
|
{
|
|
var error = lookup.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
_logger.LogError("GetMixWaveform lookup failed for {EntryKey}: {Error}", entryKey, error);
|
|
return StatusCode(500, "Failed to load release");
|
|
}
|
|
|
|
var release = lookup.Value;
|
|
var waveformEntryKey = release?.MixMetadata?.WaveformEntryKey;
|
|
if (release is null || release.Medium != ReleaseMedium.Mix || string.IsNullOrEmpty(waveformEntryKey))
|
|
{
|
|
_logger.LogInformation("No mix waveform datum for release: {EntryKey}", entryKey);
|
|
return NotFound();
|
|
}
|
|
|
|
var bytes = await _waveformProfileService.GetProfileAsync(waveformEntryKey, VaultConstants.TrackWaveforms);
|
|
if (bytes is null)
|
|
{
|
|
_logger.LogInformation("Mix waveform key set but no datum stored for release: {EntryKey}", entryKey);
|
|
return NotFound();
|
|
}
|
|
|
|
return Ok(new WaveformProfileDto
|
|
{
|
|
BucketCount = bytes.Length,
|
|
Data = Convert.ToBase64String(bytes),
|
|
});
|
|
}
|
|
|
|
// POST api/release/{id}/mix/waveform ([ApiKeyAuthorize], no body)
|
|
// Server-side trigger: fetch the Mix's track audio from the vault, compute a 2048-bucket waveform,
|
|
// store it in the mix-waveforms vault, and set MixMetadata.WaveformEntryKey. 404 when the release is
|
|
// missing or has no stored audio; 500 on compute/storage failure. Declared before "{id:long}".
|
|
[ApiKeyAuthorize]
|
|
[HttpPost("{id:long}/mix/waveform")]
|
|
public async Task<ActionResult> GenerateMixWaveform(long id, CancellationToken ct = default)
|
|
{
|
|
var result = await _unifiedReleaseService.TriggerMixWaveformAsync(id, ct);
|
|
if (result.Success)
|
|
return Ok();
|
|
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
if (string.Equals(error, ReleaseManager.ReleaseNotFoundMessage, StringComparison.Ordinal)
|
|
|| string.Equals(error, UnifiedReleaseService.MixTrackNoAudioMessage, StringComparison.Ordinal)
|
|
|| string.Equals(error, UnifiedReleaseService.MixHasNoTrackMessage, StringComparison.Ordinal))
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
_logger.LogError("GenerateMixWaveform failed for {ReleaseId}: {Error}", id, error);
|
|
return StatusCode(500, error);
|
|
}
|
|
|
|
// POST api/release/{id}/session/hero-image ([ApiKeyAuthorize], multipart)
|
|
// Stores a hero image in the images vault and sets SessionMetadata.HeroImageEntryKey. The release
|
|
// must be a Session medium (enforced in the service). Declared before "{id:long}".
|
|
[ApiKeyAuthorize]
|
|
[HttpPost("{id:long}/session/hero-image")]
|
|
[RequestSizeLimit(50_000_000)]
|
|
public async Task<ActionResult> UploadSessionHeroImage(
|
|
long id,
|
|
[FromForm] IFormFile? image,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (image is null || image.Length == 0)
|
|
return BadRequest("Image file is required");
|
|
|
|
if (MimeTypeExtensions.GetExtension(image.ContentType) == ".bin")
|
|
{
|
|
_logger.LogWarning("UploadSessionHeroImage rejected: unsupported content type '{ContentType}'", image.ContentType);
|
|
return BadRequest($"Unsupported image content type: {image.ContentType}");
|
|
}
|
|
|
|
var result = await _unifiedReleaseService.SetHeroImageAsync(id, image, ct);
|
|
if (result.Success)
|
|
return Ok();
|
|
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
if (string.Equals(error, ReleaseManager.ReleaseNotFoundMessage, StringComparison.Ordinal))
|
|
return NotFound();
|
|
|
|
_logger.LogError("UploadSessionHeroImage failed for {ReleaseId}: {Error}", id, error);
|
|
return StatusCode(500, error);
|
|
}
|
|
|
|
// GET api/release/{entryKey} (unauthenticated)
|
|
// Single release with both metadata navs (nulls for non-matching media). Public read — addresses by
|
|
// the opaque EntryKey, not the int PK (§3e). Declared after the longer "{entryKey}/mix/waveform"
|
|
// route so the segmented route resolves first.
|
|
[HttpGet("{entryKey}")]
|
|
public async Task<ActionResult> GetReleaseByEntryKey(string entryKey, CancellationToken ct = default)
|
|
{
|
|
var result = await _releaseService.GetByEntryKeyAsync(entryKey, ct);
|
|
if (!result.Success)
|
|
{
|
|
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
_logger.LogError("GetReleaseByEntryKey failed for {EntryKey}: {Error}", entryKey, error);
|
|
return StatusCode(500, "Failed to load release");
|
|
}
|
|
|
|
if (result.Value is null)
|
|
return NotFound();
|
|
|
|
return Ok(result.Value);
|
|
}
|
|
}
|