diff --git a/DeepDrftPublic.Client/Program.cs b/DeepDrftPublic.Client/Program.cs
index 300c3bc..0ce9948 100644
--- a/DeepDrftPublic.Client/Program.cs
+++ b/DeepDrftPublic.Client/Program.cs
@@ -4,17 +4,13 @@ using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
-Console.WriteLine(builder.HostEnvironment.BaseAddress);
-
-var contentApiUrl = builder.Configuration["ApiUrls:ContentApi"] ?? "https://localhost:7001";
-var sqlApiUrl = builder.Configuration["ApiUrls:SqlApi"] ?? "https://localhost:5002";
-
builder.Services.AddMudServices();
-Startup.ConfigureApiHttpClient(builder.Services, sqlApiUrl);
-Startup.ConfigureContentServices(builder.Services, contentApiUrl);
+// Both named clients point at this host's own origin. DeepDrftPublic proxies the
+// public track routes to DeepDrftAPI internally so the browser never makes a
+// cross-origin request.
+Startup.ConfigureApiHttpClient(builder.Services, builder.HostEnvironment.BaseAddress);
+Startup.ConfigureContentServices(builder.Services, builder.HostEnvironment.BaseAddress);
Startup.ConfigureDomainServices(builder.Services);
-var app = builder.Build();
-
-await app.RunAsync();
+await builder.Build().RunAsync();
diff --git a/DeepDrftPublic.Client/wwwroot/appsettings.json b/DeepDrftPublic.Client/wwwroot/appsettings.json
index 0ef5f55..0c208ae 100644
--- a/DeepDrftPublic.Client/wwwroot/appsettings.json
+++ b/DeepDrftPublic.Client/wwwroot/appsettings.json
@@ -4,9 +4,5 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
- },
- "ApiUrls": {
- "ContentApi": "https://media.deepdrft.com/",
- "SqlApi": "https://api.deepdrft.com/"
}
}
diff --git a/DeepDrftPublic/Controllers/TrackProxyController.cs b/DeepDrftPublic/Controllers/TrackProxyController.cs
new file mode 100644
index 0000000..8056156
--- /dev/null
+++ b/DeepDrftPublic/Controllers/TrackProxyController.cs
@@ -0,0 +1,108 @@
+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/[controller]")]
+public class TrackController : ControllerBase
+{
+ private readonly HttpClient _upstream;
+ private readonly ILogger _logger;
+
+ public TrackController(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 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);
+ return File(stream, contentType, enableRangeProcessing: false);
+ }
+}
diff --git a/DeepDrftPublic/Program.cs b/DeepDrftPublic/Program.cs
index c5d0a88..69dc7f7 100644
--- a/DeepDrftPublic/Program.cs
+++ b/DeepDrftPublic/Program.cs
@@ -11,13 +11,17 @@ builder.Services.AddMudServices();
var contentApiUrl = builder.Configuration["ApiUrls:ContentApi"] ?? throw new Exception("Content API URL is not configured");
var sqlApiUrl = builder.Configuration["ApiUrls:SqlApi"] ?? throw new Exception("ApiUrls:SqlApi is not configured");
+// Server-side, both named clients point straight at DeepDrftAPI (server-to-server,
+// no proxy hop). The TrackController below reuses the "DeepDrft.API" client to forward
+// the WASM client's public track calls upstream.
DeepDrftPublic.Client.Startup.ConfigureApiHttpClient(builder.Services, sqlApiUrl);
-DeepDrftPublic.Client.Startup.ConfigureDomainServices(builder.Services);
DeepDrftPublic.Client.Startup.ConfigureContentServices(builder.Services, contentApiUrl);
+DeepDrftPublic.Client.Startup.ConfigureDomainServices(builder.Services);
Startup.ConfigureDomainServices(builder);
// Add services to the container.
+builder.Services.AddControllers();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
@@ -99,6 +103,8 @@ if (app.Environment.IsDevelopment())
});
}
+app.MapControllers();
+
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()