diff --git a/DeepDrftAPI/Controllers/TrackController.cs b/DeepDrftAPI/Controllers/TrackController.cs index b9414c3..a44d536 100644 --- a/DeepDrftAPI/Controllers/TrackController.cs +++ b/DeepDrftAPI/Controllers/TrackController.cs @@ -5,7 +5,9 @@ using DeepDrftContent.Audio; using DeepDrftContent.Constants; using DeepDrftContent.FileDatabase.Services; using DeepDrftContent.FileDatabase.Models; +using DeepDrftContent.Processors; using DeepDrftData; +using DeepDrftModels.DTOs; using Microsoft.AspNetCore.Mvc; namespace DeepDrftAPI.Controllers; @@ -18,6 +20,7 @@ public class TrackController : ControllerBase private readonly WavOffsetService _wavOffsetService; private readonly UnifiedTrackService _unifiedService; private readonly ITrackService _sqlTrackService; + private readonly WaveformProfileService _waveformProfileService; private readonly ILogger _logger; // FileDatabase is injected directly for PutTrack because that endpoint receives a pre-processed @@ -32,6 +35,7 @@ public class TrackController : ControllerBase WavOffsetService wavOffsetService, UnifiedTrackService unifiedService, ITrackService sqlTrackService, + WaveformProfileService waveformProfileService, ILogger logger) { _trackContentService = trackContentService; @@ -39,6 +43,7 @@ public class TrackController : ControllerBase _wavOffsetService = wavOffsetService; _unifiedService = unifiedService; _sqlTrackService = sqlTrackService; + _waveformProfileService = waveformProfileService; _logger = logger; } @@ -348,6 +353,28 @@ public class TrackController : ControllerBase } } + // GET api/track/{trackId}/waveform (unauthenticated) + // Returns the stored waveform loudness profile for a track, base64-encoded. Public listener + // data, same auth posture as GET api/track/{trackId} streaming. 404 when no profile is stored + // (existing tracks predate profiling, or computation failed at upload — the frontend falls back + // to a flat seekbar). The "waveform" literal suffix keeps this distinct from the audio route. + [HttpGet("{trackId}/waveform")] + public async Task GetWaveform(string trackId) + { + var bytes = await _waveformProfileService.GetProfileAsync(trackId); + if (bytes is null) + { + _logger.LogInformation("No waveform profile for track: {TrackId}", trackId); + return NotFound(); + } + + return Ok(new WaveformProfileDto + { + BucketCount = bytes.Length, + Data = Convert.ToBase64String(bytes), + }); + } + [ApiKeyAuthorize] [HttpPut("{trackId}")] public async Task PutTrack(string trackId, [FromBody] AudioBinaryDto track) diff --git a/DeepDrftPublic.Client/Clients/TrackMediaClient.cs b/DeepDrftPublic.Client/Clients/TrackMediaClient.cs index b40b9b8..e2c981d 100644 --- a/DeepDrftPublic.Client/Clients/TrackMediaClient.cs +++ b/DeepDrftPublic.Client/Clients/TrackMediaClient.cs @@ -1,3 +1,6 @@ +using System.Net; +using System.Net.Http.Json; +using DeepDrftModels.DTOs; using Microsoft.Extensions.DependencyInjection; using NetBlocks.Models; @@ -61,4 +64,36 @@ public class TrackMediaClient return ApiResult.CreateFailResult(e.Message); } } + + /// + /// Fetches a track's stored waveform loudness profile. A 404 means no profile is stored + /// (existing tracks predate profiling, or computation failed at upload); callers treat that + /// as "render a flat seekbar" rather than an error, so it surfaces as a fail result with a + /// stable message rather than throwing. + /// + public async Task> GetWaveformProfileAsync(string trackId, CancellationToken cancellationToken = default) + { + try + { + var response = await _http.GetAsync($"api/track/{trackId}/waveform", cancellationToken); + if (response.StatusCode == HttpStatusCode.NotFound) + { + return ApiResult.CreateFailResult("No waveform profile available"); + } + + response.EnsureSuccessStatusCode(); + + var profile = await response.Content.ReadFromJsonAsync(); + if (profile is null) + { + return ApiResult.CreateFailResult("Waveform profile response was empty"); + } + + return ApiResult.CreatePassResult(profile); + } + catch (Exception e) + { + return ApiResult.CreateFailResult(e.Message); + } + } } diff --git a/DeepDrftPublic/Controllers/TrackProxyController.cs b/DeepDrftPublic/Controllers/TrackProxyController.cs index 5d1af5a..694921d 100644 --- a/DeepDrftPublic/Controllers/TrackProxyController.cs +++ b/DeepDrftPublic/Controllers/TrackProxyController.cs @@ -106,4 +106,38 @@ public class TrackProxyController : ControllerBase 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"); + } + } }