using Microsoft.AspNetCore.Mvc; namespace DeepDrftPublic.Controllers; /// /// 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. /// [ApiController] [Route("api/track")] public class TrackProxyController : ControllerBase { private readonly HttpClient _upstream; private readonly ILogger _logger; public TrackProxyController(IHttpClientFactory httpClientFactory, ILogger logger) { _upstream = httpClientFactory.CreateClient("DeepDrft.API"); _logger = logger; } /// Proxies paged track metadata from DeepDrftAPI. [HttpGet("page")] public async Task GetPage( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? sortColumn = null, [FromQuery] bool sortDescending = false, CancellationToken ct = default) { var query = $"api/track/page?page={page}&pageSize={pageSize}&sortDescending={sortDescending}"; if (!string.IsNullOrWhiteSpace(sortColumn)) query += $"&sortColumn={Uri.EscapeDataString(sortColumn)}"; 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"); } } /// /// 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. /// [HttpGet("meta/by-key/{entryKey}")] public async Task 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"); } } /// /// Proxies audio streaming from DeepDrftAPI. Passes the optional byte offset /// so seek-beyond-buffer works through the proxy without buffering. /// [HttpGet("{trackId}")] public async Task GetTrack( string trackId, [FromQuery] long offset = 0, CancellationToken ct = default) { _logger.LogInformation("Proxying track {TrackId} offset {Offset}", trackId, offset); var path = offset == 0 ? $"api/track/{Uri.EscapeDataString(trackId)}" : $"api/track/{Uri.EscapeDataString(trackId)}?offset={offset}"; HttpResponseMessage upstream; try { upstream = await _upstream.GetAsync(path, HttpCompletionOption.ResponseHeadersRead, ct); } catch (Exception ex) { _logger.LogError(ex, "Upstream call to DeepDrftAPI track/{TrackId} failed", trackId); return StatusCode(502, "Upstream unavailable"); } if (!upstream.IsSuccessStatusCode) { upstream.Dispose(); _logger.LogWarning("DeepDrftAPI track/{TrackId} returned {Status}", trackId, (int)upstream.StatusCode); return StatusCode((int)upstream.StatusCode); } // Do NOT dispose upstream here — File() takes ownership of the response stream // and disposes it after the body is sent. var contentType = upstream.Content.Headers.ContentType?.ToString() ?? "audio/wav"; var contentLength = upstream.Content.Headers.ContentLength; // Forward Content-Length so the WASM player has duration info from the WAV header length. if (contentLength.HasValue) Response.ContentLength = contentLength.Value; var stream = await upstream.Content.ReadAsStreamAsync(ct); HttpContext.Response.RegisterForDispose(upstream); return File(stream, contentType, enableRangeProcessing: false); } /// /// 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. /// [HttpGet("{trackId}/waveform")] public async Task 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"); } } }