feat: add search/album/genre filtering and /albums + /genres browse pages

This commit is contained in:
daniel-c-harvey
2026-06-10 10:54:56 -04:00
parent 1071ba7374
commit 5cae83b9ed
24 changed files with 940 additions and 15 deletions
@@ -22,18 +22,27 @@ public class TrackProxyController : ControllerBase
_logger = logger;
}
/// <summary>Proxies paged track metadata from DeepDrftAPI.</summary>
/// <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,
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)}";
HttpResponseMessage upstream;
try
@@ -92,6 +101,70 @@ public class TrackProxyController : ControllerBase
}
}
/// <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