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()