Add waveform profile HTTP transport: API endpoint, public proxy, content client method

This commit is contained in:
daniel-c-harvey
2026-06-05 16:57:42 -04:00
parent 9d39843982
commit de4583b759
3 changed files with 96 additions and 0 deletions
@@ -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<TrackController> _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<TrackController> 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<ActionResult> 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<ActionResult> PutTrack(string trackId, [FromBody] AudioBinaryDto track)
@@ -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<TrackMediaResponse>.CreateFailResult(e.Message);
}
}
/// <summary>
/// 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.
/// </summary>
public async Task<ApiResult<WaveformProfileDto>> GetWaveformProfileAsync(string trackId, CancellationToken cancellationToken = default)
{
try
{
var response = await _http.GetAsync($"api/track/{trackId}/waveform", cancellationToken);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return ApiResult<WaveformProfileDto>.CreateFailResult("No waveform profile available");
}
response.EnsureSuccessStatusCode();
var profile = await response.Content.ReadFromJsonAsync<WaveformProfileDto>();
if (profile is null)
{
return ApiResult<WaveformProfileDto>.CreateFailResult("Waveform profile response was empty");
}
return ApiResult<WaveformProfileDto>.CreatePassResult(profile);
}
catch (Exception e)
{
return ApiResult<WaveformProfileDto>.CreateFailResult(e.Message);
}
}
}
@@ -106,4 +106,38 @@ public class TrackProxyController : ControllerBase
HttpContext.Response.RegisterForDispose(upstream);
return File(stream, contentType, enableRangeProcessing: false);
}
/// <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");
}
}
}