a19a734757
Add GET api/track/{trackEntryKey}/waveform/high-res (+ proxy), ITrackDataService.GetTrackWaveform; rewire visualizer to resolve the current track's EntryKey and re-fetch on track change. Retire the client mix-waveform read path.
359 lines
15 KiB
C#
359 lines
15 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace DeepDrftPublic.Controllers;
|
|
|
|
/// <summary>
|
|
/// Proxies public track API calls to DeepDrftAPI so the browser never makes
|
|
/// cross-origin requests. The WASM client points both named HttpClients at
|
|
/// this host; this controller forwards unauthenticated public routes upstream.
|
|
/// SSR prerender calls DeepDrftAPI directly (server-to-server) via the same
|
|
/// named clients — no proxy hop needed on the server side.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/track")]
|
|
public class TrackProxyController : ControllerBase
|
|
{
|
|
private readonly HttpClient _upstream;
|
|
private readonly ILogger<TrackProxyController> _logger;
|
|
|
|
public TrackProxyController(IHttpClientFactory httpClientFactory, ILogger<TrackProxyController> logger)
|
|
{
|
|
_upstream = httpClientFactory.CreateClient("DeepDrft.API");
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>Proxies paged track metadata from DeepDrftAPI, forwarding optional filter params.</summary>
|
|
[HttpGet("page")]
|
|
public async Task<ActionResult> GetPage(
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20,
|
|
[FromQuery] string? sortColumn = null,
|
|
[FromQuery] bool sortDescending = false,
|
|
[FromQuery] string? q = null,
|
|
[FromQuery] string? album = null,
|
|
[FromQuery] string? genre = null,
|
|
[FromQuery] long? releaseId = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var query = $"api/track/page?page={page}&pageSize={pageSize}&sortDescending={sortDescending}";
|
|
if (!string.IsNullOrWhiteSpace(sortColumn))
|
|
query += $"&sortColumn={Uri.EscapeDataString(sortColumn)}";
|
|
if (!string.IsNullOrWhiteSpace(q))
|
|
query += $"&q={Uri.EscapeDataString(q)}";
|
|
if (!string.IsNullOrWhiteSpace(album))
|
|
query += $"&album={Uri.EscapeDataString(album)}";
|
|
if (!string.IsNullOrWhiteSpace(genre))
|
|
query += $"&genre={Uri.EscapeDataString(genre)}";
|
|
if (releaseId is { } rid)
|
|
query += $"&releaseId={rid}";
|
|
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync(query, HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/page failed");
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/page returned {Status}", (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies the random-track metadata lookup from DeepDrftAPI. Unauthenticated, same posture as
|
|
/// the paged listing. Small JSON, buffered and relayed; a 404 from upstream (empty library)
|
|
/// passes through so the client renders it as a valid empty state. Declared before the
|
|
/// parameterized "{trackId}" route so the literal segment is never treated as a trackId.
|
|
/// </summary>
|
|
[HttpGet("random")]
|
|
public async Task<ActionResult> GetRandom(CancellationToken ct = default)
|
|
{
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync("api/track/random", HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/random failed");
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/random returned {Status}", (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies the distinct-albums browse list from DeepDrftAPI. Unauthenticated, same posture as
|
|
/// the paged listing. Small JSON, buffered and relayed. Literal segment, declared before the
|
|
/// parameterized "{trackId}" route so it is never treated as a trackId.
|
|
/// </summary>
|
|
[HttpGet("albums")]
|
|
public async Task<ActionResult> GetAlbums(CancellationToken ct = default)
|
|
{
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync("api/track/albums", HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/albums failed");
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/albums returned {Status}", (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies the distinct-genres browse list from DeepDrftAPI. Unauthenticated, same posture as
|
|
/// the paged listing. Small JSON, buffered and relayed. Literal segment, declared before the
|
|
/// parameterized "{trackId}" route so it is never treated as a trackId.
|
|
/// </summary>
|
|
[HttpGet("genres")]
|
|
public async Task<ActionResult> GetGenres(CancellationToken ct = default)
|
|
{
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync("api/track/genres", HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/genres failed");
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/genres returned {Status}", (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies single-track metadata lookup by vault entry key from DeepDrftAPI. Unauthenticated,
|
|
/// same posture as the paged listing. Small JSON, so it is buffered and relayed; a 404 from
|
|
/// upstream (no track with that entry key) passes through. Declared before the parameterized
|
|
/// "{trackId}" route, though the 3-segment template makes a collision impossible regardless.
|
|
/// </summary>
|
|
[HttpGet("meta/by-key/{entryKey}")]
|
|
public async Task<ActionResult> GetMetaByKey(string entryKey, CancellationToken ct = default)
|
|
{
|
|
var path = $"api/track/meta/by-key/{Uri.EscapeDataString(entryKey)}";
|
|
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/meta/by-key/{EntryKey} failed", entryKey);
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/meta/by-key/{EntryKey} returned {Status}", entryKey, (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies audio streaming from DeepDrftAPI as a transparent HTTP Range relay.
|
|
/// Forwards the incoming Range header upstream and relays the upstream status
|
|
/// (200 full, 206 partial, 416 unsatisfiable) and range-related response headers
|
|
/// back to the browser verbatim. The proxy does not slice — the upstream already did.
|
|
/// </summary>
|
|
[HttpGet("{trackId}")]
|
|
public async Task<ActionResult> GetTrack(
|
|
string trackId,
|
|
CancellationToken ct = default)
|
|
{
|
|
var rangeHeader = Request.Headers.Range.ToString();
|
|
_logger.LogInformation("Proxying track {TrackId} range '{Range}'", trackId, rangeHeader);
|
|
|
|
var request = new HttpRequestMessage(
|
|
HttpMethod.Get,
|
|
$"api/track/{Uri.EscapeDataString(trackId)}");
|
|
|
|
// Forward the browser's Range header upstream so DeepDrftAPI slices the file.
|
|
// TryAddWithoutValidation avoids RangeHeaderValue reparsing — we relay the raw
|
|
// header verbatim, keeping the proxy transparent.
|
|
if (!string.IsNullOrEmpty(rangeHeader))
|
|
request.Headers.TryAddWithoutValidation("Range", rangeHeader);
|
|
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/{TrackId} failed", trackId);
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
// 416 Range Not Satisfiable is a legitimate upstream answer (seek past EOF);
|
|
// relay it as-is rather than collapsing it into a 502.
|
|
if ((int)upstream.StatusCode == StatusCodes.Status416RangeNotSatisfiable)
|
|
{
|
|
upstream.Dispose();
|
|
return StatusCode(StatusCodes.Status416RangeNotSatisfiable);
|
|
}
|
|
|
|
// 206 Partial Content reports IsSuccessStatusCode == true, so this guard only
|
|
// catches genuine upstream failures (404, 5xx).
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
upstream.Dispose();
|
|
_logger.LogWarning("DeepDrftAPI track/{TrackId} returned {Status}", trackId, (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
// Stream the body manually rather than via File(): FileStreamResult forces the
|
|
// status to 200 (with enableRangeProcessing:false) and would clobber a relayed
|
|
// 206. Writing status + headers + body directly keeps the partial-content
|
|
// contract intact, with the proxy doing zero slicing of its own.
|
|
HttpContext.Response.RegisterForDispose(upstream);
|
|
Response.StatusCode = (int)upstream.StatusCode;
|
|
Response.ContentType = upstream.Content.Headers.ContentType?.ToString() ?? "audio/wav";
|
|
|
|
// Relay range-related headers so the browser sees the same partial-content
|
|
// contract the upstream emitted.
|
|
if (upstream.Headers.AcceptRanges.Count > 0)
|
|
Response.Headers.AcceptRanges = string.Join(", ", upstream.Headers.AcceptRanges);
|
|
if (upstream.Content.Headers.ContentRange is { } contentRange)
|
|
Response.Headers.ContentRange = contentRange.ToString();
|
|
if (upstream.Content.Headers.ContentLength is { } contentLength)
|
|
Response.ContentLength = contentLength;
|
|
|
|
var stream = await upstream.Content.ReadAsStreamAsync(ct);
|
|
try
|
|
{
|
|
await stream.CopyToAsync(Response.Body, ct);
|
|
return new EmptyResult();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Client navigated away or issued a new seek — nothing to do.
|
|
// The upstream connection is cleaned up via RegisterForDispose.
|
|
return new EmptyResult();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies a track's stored waveform profile (JSON) from DeepDrftAPI. Unauthenticated,
|
|
/// same posture as the audio stream forward. The profile is small JSON, so it is buffered
|
|
/// and relayed rather than streamed; a 404 from upstream (no profile stored) passes through.
|
|
/// </summary>
|
|
[HttpGet("{trackId}/waveform")]
|
|
public async Task<ActionResult> GetWaveform(string trackId, CancellationToken ct = default)
|
|
{
|
|
var path = $"api/track/{Uri.EscapeDataString(trackId)}/waveform";
|
|
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/{TrackId}/waveform failed", trackId);
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/{TrackId}/waveform returned {Status}", trackId, (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Proxies a track's high-res waveform datum (JSON) from DeepDrftAPI — the per-track datum the lava
|
|
/// visualizer fetches for the current track (phase-12 §5b). Unauthenticated, same posture as the
|
|
/// 512-bucket profile forward above; the "high-res" suffix selects the TrackWaveforms datum. Small
|
|
/// JSON, buffered and relayed; a 404 (no high-res datum stored — track not yet backfilled) passes
|
|
/// through so the visualizer blanks gracefully.
|
|
/// </summary>
|
|
[HttpGet("{trackId}/waveform/high-res")]
|
|
public async Task<ActionResult> GetHighResWaveform(string trackId, CancellationToken ct = default)
|
|
{
|
|
var path = $"api/track/{Uri.EscapeDataString(trackId)}/waveform/high-res";
|
|
|
|
HttpResponseMessage upstream;
|
|
try
|
|
{
|
|
upstream = await _upstream.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ct);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Upstream call to DeepDrftAPI track/{TrackId}/waveform/high-res failed", trackId);
|
|
return StatusCode(502, "Upstream unavailable");
|
|
}
|
|
|
|
using (upstream)
|
|
{
|
|
if (!upstream.IsSuccessStatusCode)
|
|
{
|
|
_logger.LogWarning("DeepDrftAPI track/{TrackId}/waveform/high-res returned {Status}", trackId, (int)upstream.StatusCode);
|
|
return StatusCode((int)upstream.StatusCode);
|
|
}
|
|
|
|
var json = await upstream.Content.ReadAsStringAsync(ct);
|
|
return Content(json, "application/json");
|
|
}
|
|
}
|
|
}
|