Merge p12-w2-t1-track-fetch into dev (12.B2: track-cardinal high-res waveform fetch + bridge rewire)
This commit is contained in:
@@ -67,12 +67,17 @@ public class ReleaseController : ControllerBase
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Serves the high-res waveform datum for a Mix release as base64, reading 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.
|
||||
//
|
||||
// LEGACY (phase-12 §5b): the visualizer no longer fetches through this release-addressed route — it
|
||||
// resolves the current track's datum via the track-cardinal GET api/track/{trackEntryKey}/waveform/
|
||||
// high-res. This endpoint is retained as a thin transitional delegate (it serves the identical datum,
|
||||
// since a Mix is single-track) and has no client caller today; remove it once nothing depends on the
|
||||
// release-addressed shape.
|
||||
[HttpGet("{entryKey}/mix/waveform")]
|
||||
public async Task<ActionResult> GetMixWaveform(string entryKey, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
@@ -577,6 +577,31 @@ public class TrackController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
// GET api/track/{trackId}/waveform/high-res (unauthenticated)
|
||||
// Track-cardinal high-res datum fetch (phase-12 §5b): returns the per-track high-res waveform datum
|
||||
// from the track-waveforms vault, base64-encoded, keyed by EntryKey. This is what the lava visualizer
|
||||
// fetches for whatever track is currently playing/selected — the release is only addressing context.
|
||||
// Distinct from GET {trackId}/waveform (the 512-bucket player-bar profile in the default vault): the
|
||||
// "high-res" suffix selects the duration-derived TrackWaveforms datum. 404 when no high-res datum is
|
||||
// stored (a track not yet backfilled — the visualizer blanks gracefully). Declared before the
|
||||
// parameterized PUT "{trackId}" route so the literal "waveform/high-res" segment wins.
|
||||
[HttpGet("{trackId}/waveform/high-res")]
|
||||
public async Task<ActionResult> GetHighResWaveform(string trackId)
|
||||
{
|
||||
var bytes = await _waveformProfileService.GetProfileAsync(trackId, VaultConstants.TrackWaveforms);
|
||||
if (bytes is null)
|
||||
{
|
||||
_logger.LogInformation("No high-res waveform datum for track: {TrackId}", trackId);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new WaveformProfileDto
|
||||
{
|
||||
BucketCount = bytes.Length,
|
||||
Data = Convert.ToBase64String(bytes),
|
||||
});
|
||||
}
|
||||
|
||||
// POST api/track/{trackId}/waveform ([ApiKeyAuthorize])
|
||||
// Admin backfill: compute and store a waveform profile for an existing track from its vault
|
||||
// audio. trackId is the EntryKey. 404 when no audio is stored under that key; 500 when the
|
||||
|
||||
@@ -105,8 +105,9 @@ public class UnifiedReleaseService
|
||||
/// <see cref="WaveformResolution"/>), store it in the TrackWaveforms vault under the track's
|
||||
/// EntryKey, then point the release's Mix satellite at that same key. The datum key equals the
|
||||
/// track's EntryKey — the Mix is single-track. Under the per-track model (phase-12 §5) this is the
|
||||
/// same datum every track now carries; the Mix satellite link is kept so the existing public
|
||||
/// <c>GET api/release/{entryKey}/mix/waveform</c> read path keeps resolving until 12.B2 rewires it.
|
||||
/// same datum every track now carries. The visualizer fetches it via the track-cardinal
|
||||
/// <c>GET api/track/{trackEntryKey}/waveform/high-res</c> (12.B2); the Mix satellite link and the
|
||||
/// legacy release-addressed read path are retained transitionally and no longer feed the visualizer.
|
||||
/// </summary>
|
||||
public async Task<Result> TriggerMixWaveformAsync(long releaseId, CancellationToken ct)
|
||||
{
|
||||
|
||||
@@ -83,27 +83,4 @@ public class ReleaseClient
|
||||
? ApiResult<ReleaseDto>.CreatePassResult(release)
|
||||
: ApiResult<ReleaseDto>.CreateFailResult("Failed to deserialize response");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the high-res waveform datum for a Mix release, addressed by its public EntryKey. A 404
|
||||
/// means no datum is stored (not yet generated, or not a Mix) — a valid state, so it returns a pass
|
||||
/// result with a null value. Any other non-success status is a genuine failure.
|
||||
/// </summary>
|
||||
public async Task<ApiResult<WaveformProfileDto?>> GetMixWaveform(string entryKey)
|
||||
{
|
||||
var response = await _http.GetAsync($"api/release/{Uri.EscapeDataString(entryKey)}/mix/waveform");
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
return ApiResult<WaveformProfileDto?>.CreatePassResult(null);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return ApiResult<WaveformProfileDto?>.CreateFailResult($"HTTP {(int)response.StatusCode}");
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var profile = JsonSerializer.Deserialize<WaveformProfileDto>(json, JsonOptions);
|
||||
|
||||
return profile is not null
|
||||
? ApiResult<WaveformProfileDto?>.CreatePassResult(profile)
|
||||
: ApiResult<WaveformProfileDto?>.CreateFailResult("Failed to deserialize response");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,33 @@ public class TrackClient
|
||||
: ApiResult<List<GenreSummaryDto>>.CreateFailResult("Failed to deserialize response");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the per-track high-res waveform datum, addressed by the track's EntryKey (phase-12 §5b).
|
||||
/// A 404 means no high-res datum is stored (a track not yet backfilled) — a valid state, so it
|
||||
/// returns a pass result with a null value and the visualizer blanks gracefully. Any other
|
||||
/// non-success status is a genuine failure.
|
||||
/// </summary>
|
||||
public async Task<ApiResult<WaveformProfileDto?>> GetTrackWaveform(string trackEntryKey)
|
||||
{
|
||||
var response = await _http.GetAsync($"api/track/{Uri.EscapeDataString(trackEntryKey)}/waveform/high-res");
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
return ApiResult<WaveformProfileDto?>.CreatePassResult(null);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return ApiResult<WaveformProfileDto?>.CreateFailResult($"HTTP {(int)response.StatusCode}");
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var profile = JsonSerializer.Deserialize<WaveformProfileDto>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
return profile is not null
|
||||
? ApiResult<WaveformProfileDto?>.CreatePassResult(profile)
|
||||
: ApiResult<WaveformProfileDto?>.CreateFailResult("Failed to deserialize response");
|
||||
}
|
||||
|
||||
public async Task<ApiResult<TrackDto>> GetTrack(string entryKey)
|
||||
{
|
||||
var response = await _http.GetAsync($"api/track/meta/by-key/{Uri.EscapeDataString(entryKey)}");
|
||||
|
||||
@@ -8,21 +8,24 @@ using Microsoft.JSInterop;
|
||||
namespace DeepDrftPublic.Client.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Full-page scrolling waveform background. Standalone and reusable: give it a
|
||||
/// <see cref="ReleaseEntryKey"/> and it fetches its own loudness datum. The rendering itself — a windowed,
|
||||
/// bottom-to-top, playback-coupled scroll with a glassy theme-aware gradient — lives in the
|
||||
/// WaveformVisualizer.ts interop module; this component is the bridge that feeds it datum, playback
|
||||
/// position, zoom, and theme, and owns the module lifecycle.
|
||||
/// Scrolling waveform visualizer, track-cardinal (phase-12 §4/§5). It renders the high-res loudness
|
||||
/// datum of whatever track is currently playing/selected: the datum is the track's, not the release's,
|
||||
/// so the fetch resolves the current track's <c>EntryKey</c> (the playing track when this is the active
|
||||
/// player, else the host-supplied <see cref="TrackEntryKey"/>) and re-fetches when that track identity
|
||||
/// changes — not when the release changes. The release (<see cref="ReleaseEntryKey"/>) is only addressing
|
||||
/// context. The rendering itself — a windowed, bottom-to-top, playback-coupled scroll with a glassy
|
||||
/// theme-aware gradient — lives in the WaveformVisualizer.ts interop module; this component is the bridge
|
||||
/// that feeds it datum, playback position, zoom, and theme, and owns the module lifecycle.
|
||||
///
|
||||
/// Strictly read-only (spec §D): no seek, no two-way write-back. <see cref="PlaybackPosition"/> is a
|
||||
/// one-way input. The live playback signal on the Mix detail page comes from the cascaded player
|
||||
/// service (which also supplies the mix duration needed for the time↔sample mapping); the
|
||||
/// <see cref="PlaybackPosition"/> parameter is the composability fallback for hosts that have no
|
||||
/// player cascade (e.g. an embed) and want to drive position themselves.
|
||||
/// one-way input. The live playback signal comes from the cascaded player service (which also supplies
|
||||
/// the track duration needed for the time↔sample mapping); the <see cref="PlaybackPosition"/> parameter
|
||||
/// is the composability fallback for hosts that have no player cascade (e.g. an embed) and want to drive
|
||||
/// position themselves.
|
||||
/// </summary>
|
||||
public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[Inject] public required IReleaseDataService ReleaseData { get; set; }
|
||||
[Inject] public required ITrackDataService TrackData { get; set; }
|
||||
[Inject] public required IJSRuntime JS { get; set; }
|
||||
[Inject] public required WaveformVisualizerControlState ControlState { get; set; }
|
||||
[Inject] public required ILogger<WaveformVisualizer> Logger { get; set; }
|
||||
@@ -36,17 +39,30 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// us, and OnAfterRender pushes fresh palette colours into the module.
|
||||
[CascadingParameter] public DarkModeSettings? DarkMode { get; set; }
|
||||
|
||||
/// <summary>The opaque public EntryKey of the Mix release whose waveform datum to fetch and render.</summary>
|
||||
/// <summary>
|
||||
/// The opaque public EntryKey of the host release. Addressing context only (phase-12 §4) — the datum
|
||||
/// is fetched per-track, not per-release. Carried for diagnostics and host identity; it no longer
|
||||
/// drives the datum fetch.
|
||||
/// </summary>
|
||||
[Parameter] public required string ReleaseEntryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of this mix's playable track. Used to gate the cascaded player as the live source: we
|
||||
/// only couple to playback when the player is on THIS track, so a different track playing
|
||||
/// elsewhere leaves this backdrop at its at-rest slice instead of scrolling to the wrong audio.
|
||||
/// Null leaves the visualizer in the at-rest state (no player coupling).
|
||||
/// The id of the host's selected/default playable track. Used to gate the cascaded player as the
|
||||
/// live source: we only couple to playback when the player is on THIS track, so a different track
|
||||
/// playing elsewhere leaves this visualizer at its at-rest slice instead of scrolling to the wrong
|
||||
/// audio. Null leaves the visualizer in the at-rest state (no player coupling).
|
||||
/// </summary>
|
||||
[Parameter] public long? TrackId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The EntryKey of the host's selected/default track — the datum to render when no matching player
|
||||
/// is active (e.g. a Mix detail page at rest, before playback starts). When the cascaded player is on
|
||||
/// this visualizer's track (<see cref="IsActivePlayer"/>), the playing track's EntryKey takes
|
||||
/// precedence so the datum follows live playback (a multi-track Cut, the NowPlaying card). Null with
|
||||
/// no active player leaves the visualizer blank.
|
||||
/// </summary>
|
||||
[Parameter] public string? TrackEntryKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized playback head in [0, 1]. One-way input only — the component never writes back.
|
||||
/// Used as the position source for hosts with no cascaded player (composability fallback);
|
||||
@@ -72,7 +88,13 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
private WaveformProfileDto? _profile;
|
||||
private string? _loadedReleaseKey;
|
||||
|
||||
// The track EntryKey the loaded datum belongs to. The fetch-once guard keys on the current track's
|
||||
// identity (phase-12 §4), not the release, so the datum re-fetches when the playing/selected track
|
||||
// changes while the release stays fixed (a multi-track Cut, the NowPlaying card). Null until the
|
||||
// first fetch; an in-flight fetch is tracked separately so concurrent ticks don't double-fetch.
|
||||
private string? _loadedTrackKey;
|
||||
private string? _fetchingTrackKey;
|
||||
|
||||
// Whether we are subscribed to the shared control state's Changed event. The controls row (a
|
||||
// sibling component) mutates ControlState and raises Changed; we push the affected uniforms.
|
||||
@@ -118,14 +140,60 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
DebugLog($"NO player cascade — playback will never couple. ReleaseEntryKey={ReleaseEntryKey}, TrackId={TrackId?.ToString() ?? "null"}.");
|
||||
}
|
||||
|
||||
// ReleaseEntryKey is the only fetch input; fetch once per key. Position/zoom/theme changes
|
||||
// re-render but must not refetch, and a release with no datum must not refetch either — so the
|
||||
// guard keys on the fetched key, not on whether a profile came back.
|
||||
if (_loadedReleaseKey == ReleaseEntryKey) return;
|
||||
_loadedReleaseKey = ReleaseEntryKey;
|
||||
// Fetch the current track's datum if its identity changed since the last fetch (parameter set
|
||||
// can change TrackEntryKey; the player side comes through OnPlayerStateChanged).
|
||||
await EnsureDatumForCurrentTrackAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The EntryKey of the track whose datum to render: the live playing track when this visualizer is
|
||||
/// the active player, otherwise the host's selected/default <see cref="TrackEntryKey"/>. This is the
|
||||
/// single source of "which track's datum" — both the fetch key and what re-arms the fetch-once guard.
|
||||
/// </summary>
|
||||
private string? CurrentTrackKey =>
|
||||
IsActivePlayer ? PlayerService!.CurrentTrack!.EntryKey : TrackEntryKey;
|
||||
|
||||
/// <summary>
|
||||
/// Fetch the current track's high-res datum, but only when the track identity changed since the last
|
||||
/// fetch (phase-12 §4 — the guard re-arms on track change, not release change). Idempotent and
|
||||
/// re-entrancy-guarded: callable from both OnParametersSetAsync (TrackEntryKey changed) and
|
||||
/// OnPlayerStateChanged (the playing track changed) without double-fetching. A track with no stored
|
||||
/// datum leaves the visualizer blank; the guard keys on the fetched key, not on whether a datum came
|
||||
/// back, so a not-yet-backfilled track does not refetch on every tick.
|
||||
/// </summary>
|
||||
private async Task EnsureDatumForCurrentTrackAsync()
|
||||
{
|
||||
var trackKey = CurrentTrackKey;
|
||||
|
||||
// Nothing to fetch (no active player and no selected track): clear any stale datum and disarm.
|
||||
if (string.IsNullOrEmpty(trackKey))
|
||||
{
|
||||
if (_loadedTrackKey is not null || _profile is not null)
|
||||
{
|
||||
_loadedTrackKey = null;
|
||||
_profile = null;
|
||||
await PushDatumAsync();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Already loaded (or loading) this exact track — don't refetch.
|
||||
if (trackKey == _loadedTrackKey || trackKey == _fetchingTrackKey) return;
|
||||
|
||||
_fetchingTrackKey = trackKey;
|
||||
DebugLog($"fetching high-res waveform datum for trackEntryKey={trackKey}…");
|
||||
var result = await TrackData.GetTrackWaveform(trackKey);
|
||||
|
||||
// The current track may have advanced again while this fetch was in flight; if so, discard this
|
||||
// result and let the newer track's fetch (already armed via _fetchingTrackKey) win.
|
||||
if (_fetchingTrackKey != trackKey)
|
||||
{
|
||||
DebugLog($"discarding stale datum fetch for trackEntryKey={trackKey} — current track moved on.");
|
||||
return;
|
||||
}
|
||||
_fetchingTrackKey = null;
|
||||
_loadedTrackKey = trackKey;
|
||||
|
||||
DebugLog($"fetching mix waveform datum for ReleaseEntryKey={ReleaseEntryKey}…");
|
||||
var result = await ReleaseData.GetMixWaveform(ReleaseEntryKey);
|
||||
if (result is { Success: true, Value: { } profile } && profile.BucketCount > 0 && profile.Data.Length > 0)
|
||||
{
|
||||
_profile = profile;
|
||||
@@ -133,12 +201,12 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
}
|
||||
else
|
||||
{
|
||||
// No datum (not generated yet, or not a Mix) — empty backdrop; the detail page still
|
||||
// No datum (track not yet backfilled, or transport error) — blank visualizer; the host still
|
||||
// renders its content over a plain background.
|
||||
_profile = null;
|
||||
DebugLog(result.Success
|
||||
? $"datum fetch returned EMPTY/absent (no stored datum for ReleaseEntryKey={ReleaseEntryKey}) — backdrop stays blank."
|
||||
: $"datum fetch FAILED ({result.GetMessage() ?? "unknown error"}) — backdrop stays blank.");
|
||||
? $"datum fetch returned EMPTY/absent (no stored high-res datum for trackEntryKey={trackKey}) — visualizer stays blank."
|
||||
: $"datum fetch FAILED ({result.GetMessage() ?? "unknown error"}) — visualizer stays blank.");
|
||||
}
|
||||
|
||||
// Push the (possibly new) datum to the module if it is already created.
|
||||
@@ -153,6 +221,10 @@ public partial class WaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// player is on THIS track (IsActivePlayer), and what duration/position/play-state it reports.
|
||||
var currentTrackId = PlayerService?.CurrentTrack is { } ct ? ct.Id.ToString() : "null";
|
||||
DebugLog($"player StateChanged — IsActivePlayer={IsActivePlayer} (player.CurrentTrack.Id={currentTrackId}, TrackId={TrackId?.ToString() ?? "null"}), player.IsPlaying={PlayerService?.IsPlaying}, player.Duration={PlayerService?.Duration?.ToString("F2") ?? "null"}.");
|
||||
// The playing track may have changed (a multi-track Cut advancing, the NowPlaying card following
|
||||
// playback) — re-fetch its datum if so. EnsureDatumForCurrentTrackAsync is guarded, so a tick that
|
||||
// didn't change the track is a cheap no-op.
|
||||
await EnsureDatumForCurrentTrackAsync();
|
||||
await PushPlaybackAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
|
||||
@@ -35,8 +35,11 @@ else
|
||||
|
||||
@* Full-page waveform sits behind the scaffold content. The scaffold's container is positioned
|
||||
above it via the mix-detail-foreground stacking context. TrackId lets the visualizer couple to
|
||||
playback only when the player is on this mix's track. *@
|
||||
<WaveformVisualizer ReleaseEntryKey="@release.EntryKey" TrackId="@ViewModel.Track?.Id" />
|
||||
playback only when the player is on this mix's track; TrackEntryKey is the datum to render at rest
|
||||
(before playback) — the mix's single track, so the lava shows immediately on page load (§4). *@
|
||||
<WaveformVisualizer ReleaseEntryKey="@release.EntryKey"
|
||||
TrackId="@ViewModel.Track?.Id"
|
||||
TrackEntryKey="@ViewModel.Track?.EntryKey" />
|
||||
|
||||
<div class="mix-detail-foreground">
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mix-detail-container">
|
||||
|
||||
@@ -24,11 +24,4 @@ public interface IReleaseDataService
|
||||
|
||||
/// <summary>Single release resolved by its opaque public EntryKey, with both metadata satellites (nulls for non-matching media).</summary>
|
||||
Task<ApiResult<ReleaseDto>> GetByEntryKey(string entryKey);
|
||||
|
||||
/// <summary>
|
||||
/// The Mix waveform datum for a release addressed by its public EntryKey. Success with a value
|
||||
/// when present; success with a null value when no datum is stored (a valid state, not a failure);
|
||||
/// failure on any other transport error.
|
||||
/// </summary>
|
||||
Task<ApiResult<WaveformProfileDto?>> GetMixWaveform(string entryKey);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ public interface ITrackDataService
|
||||
|
||||
Task<ApiResult<TrackDto>> GetTrack(string trackId);
|
||||
|
||||
/// <summary>
|
||||
/// The per-track high-res waveform datum, addressed by the track's EntryKey (phase-12 §5b — the
|
||||
/// datum is the track's; the release is only addressing context). Success with a value when a
|
||||
/// high-res datum is stored; success with a null value when none is (a not-yet-backfilled track —
|
||||
/// a valid state, not a failure, the visualizer blanks); failure on any other transport error.
|
||||
/// </summary>
|
||||
Task<ApiResult<WaveformProfileDto?>> GetTrackWaveform(string trackEntryKey);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a random track from the public library for instant play. Success with a value on a
|
||||
/// hit; success with a null value when the library is empty (a valid state, not a failure);
|
||||
|
||||
@@ -31,7 +31,4 @@ public class ReleaseClientDataService : IReleaseDataService
|
||||
|
||||
public Task<ApiResult<ReleaseDto>> GetByEntryKey(string entryKey)
|
||||
=> _releaseClient.GetByEntryKey(entryKey);
|
||||
|
||||
public Task<ApiResult<WaveformProfileDto?>> GetMixWaveform(string entryKey)
|
||||
=> _releaseClient.GetMixWaveform(entryKey);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ public class TrackClientDataService : ITrackDataService
|
||||
public Task<ApiResult<TrackDto>> GetTrack(string trackId)
|
||||
=> _trackClient.GetTrack(trackId);
|
||||
|
||||
public Task<ApiResult<WaveformProfileDto?>> GetTrackWaveform(string trackEntryKey)
|
||||
=> _trackClient.GetTrackWaveform(trackEntryKey);
|
||||
|
||||
public Task<ApiResult<TrackDto?>> GetRandomTrack()
|
||||
=> _trackClient.GetRandom();
|
||||
}
|
||||
|
||||
@@ -47,11 +47,6 @@ public class ReleaseProxyController : ControllerBase
|
||||
return await RelayJson(query, "release list");
|
||||
}
|
||||
|
||||
/// <summary>Proxies the Mix waveform datum, addressed by the release's opaque EntryKey. A 404 (no datum stored) passes through verbatim.</summary>
|
||||
[HttpGet("{entryKey}/mix/waveform")]
|
||||
public async Task<ActionResult> GetMixWaveform(string entryKey, CancellationToken ct = default)
|
||||
=> await RelayJson($"api/release/{Uri.EscapeDataString(entryKey)}/mix/waveform", $"release {entryKey} mix waveform", ct);
|
||||
|
||||
/// <summary>Proxies a single release, addressed by its opaque EntryKey. A 404 (no such release) passes through verbatim.</summary>
|
||||
[HttpGet("{entryKey}")]
|
||||
public async Task<ActionResult> GetReleaseByEntryKey(string entryKey, CancellationToken ct = default)
|
||||
|
||||
@@ -319,4 +319,40 @@ public class TrackProxyController : ControllerBase
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user