diff --git a/allstarr.Tests/Providers/AppleDownloadCapabilityAdapterTests.cs b/allstarr.Tests/Providers/AppleDownloadCapabilityAdapterTests.cs index 95df115f..f3634a7b 100644 --- a/allstarr.Tests/Providers/AppleDownloadCapabilityAdapterTests.cs +++ b/allstarr.Tests/Providers/AppleDownloadCapabilityAdapterTests.cs @@ -148,7 +148,7 @@ public sealed class AppleDownloadCapabilityAdapterTests : IDisposable "apple/track 1"); var lease = (await adapter.GetStreamLeaseAsync( - Context(tenant, user), new(track))).RequireValue(); + Context(tenant, user), new(track, ProviderAudioQuality.HighResolution))).RequireValue(); using var request = new HttpRequestMessage(HttpMethod.Get, lease.ProtectedSourceUri); using var response = await lease.ProtectedResponseFactory!(request, CancellationToken.None); @@ -162,7 +162,7 @@ public sealed class AppleDownloadCapabilityAdapterTests : IDisposable Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(audio, await response.Content.ReadAsByteArrayAsync()); Assert.Contains(gateway.Requests, uri => - uri.PathAndQuery == "/api/stream/apple%2Ftrack%201?quality=aac-320"); + uri.PathAndQuery == "/api/stream/apple%2Ftrack%201?quality=alac-24-96"); } [Fact] diff --git a/allstarr/Core/Providers/AppleDownload/AppleDownloadStreamingCapabilityAdapter.cs b/allstarr/Core/Providers/AppleDownload/AppleDownloadStreamingCapabilityAdapter.cs index 4cae1529..55714160 100644 --- a/allstarr/Core/Providers/AppleDownload/AppleDownloadStreamingCapabilityAdapter.cs +++ b/allstarr/Core/Providers/AppleDownload/AppleDownloadStreamingCapabilityAdapter.cs @@ -47,12 +47,7 @@ public sealed class AppleDownloadStreamingCapabilityAdapter : IProviderStreaming return ProviderOutcome.Failure(new(ErrorFor(snapshot.State))); if (!OutboundRequestGuard.TryCreateConfiguredServiceUri(settings.BaseUrl, out var baseUri, out _)) return ProviderOutcome.Failure(new(ProviderErrorKind.AccountNeedsConfiguration)); - // Cold lossless preparation can outlast music-client startup timeouts. - // Keep configured lossless quality for downloads; default playback starts at AAC 320. - var playbackQuality = request.RequestedQuality == ProviderAudioQuality.Any - ? ProviderAudioQuality.Lossy - : request.RequestedQuality; - var quality = AppleDownloadCapabilityAdapter.Quality(playbackQuality, settings.Quality); + var quality = AppleDownloadCapabilityAdapter.Quality(request.RequestedQuality, settings.Quality); var source = new Uri(baseUri!, $"api/stream/{Uri.EscapeDataString(request.TrackId.Value)}?quality={Uri.EscapeDataString(quality)}"); return ProviderOutcome.Success(new( diff --git a/sidecars/apple-gateway/apple_gateway/app.py b/sidecars/apple-gateway/apple_gateway/app.py index ace1e622..25c27d29 100644 --- a/sidecars/apple-gateway/apple_gateway/app.py +++ b/sidecars/apple-gateway/apple_gateway/app.py @@ -5,7 +5,7 @@ import shutil import uuid from contextlib import asynccontextmanager from pathlib import Path -from typing import Any +from typing import Any, AsyncIterator import httpx from fastapi import FastAPI, HTTPException, Query @@ -274,12 +274,24 @@ def create_app( @application.get("/api/stream/{song_id}") async def stream_song(song_id: str, quality: str = "alac-16-44") -> StreamingResponse: - root, source = await prepare_song(song_id, quality, "aac-web") + async def content() -> AsyncIterator[bytes]: + root: Path | None = None + try: + root, source = await prepare_song(song_id, quality, "aac-web") + async for chunk in process_runner.stream_flac(source): + yield chunk + finally: + if root is not None: + shutil.rmtree(root, ignore_errors=True) + return StreamingResponse( - process_runner.stream_flac(source), + content(), media_type="audio/flac", - headers={"Content-Disposition": f'inline; filename="{song_id}.flac"'}, - background=BackgroundTask(shutil.rmtree, root, ignore_errors=True), + headers={ + "Content-Disposition": f'inline; filename="{song_id}.flac"', + "Cache-Control": "no-store", + "X-Accel-Buffering": "no", + }, ) @application.head("/api/stream/{song_id}") diff --git a/sidecars/apple-gateway/tests/test_gateway.py b/sidecars/apple-gateway/tests/test_gateway.py index 43a9a896..c304771d 100644 --- a/sidecars/apple-gateway/tests/test_gateway.py +++ b/sidecars/apple-gateway/tests/test_gateway.py @@ -10,6 +10,7 @@ from typing import Any import httpx import pytest from fastapi.testclient import TestClient +from fastapi.responses import StreamingResponse from apple_gateway.app import API_VERSION, create_app from apple_gateway.catalog import CatalogClient @@ -247,6 +248,22 @@ def test_song_stream_head_reports_only_known_facts_without_preparing_media(clien assert client[0].head("/api/stream/102", params={"quality": "made-up"}).status_code == 400 +@pytest.mark.asyncio +async def test_song_stream_opens_before_preparing_configured_quality(settings: Settings): + runner = FakeRunner() + app = create_app(settings, FakeWrapper(), FakeCatalog(), runner) + route = next(route for route in app.routes if getattr(route, "path", None) == "/api/stream/{song_id}") + + response = await route.endpoint("102", "alac-16-44") + + assert isinstance(response, StreamingResponse) + assert response.headers["cache-control"] == "no-store" + assert response.headers["x-accel-buffering"] == "no" + assert runner.calls == [] + assert b"".join([chunk async for chunk in response.body_iterator]) == b"fLaCfixture" + assert runner.calls[-1][1] == "alac" + + def test_song_stream_falls_back_to_web_aac_when_lossless_is_unavailable(settings): class FallbackRunner(FakeRunner): async def download(self, url: str, quality: str, output: Path, temporary: Path) -> list[Path]: