fix(playback): preserve Apple stream quality on startup

This commit is contained in:
joshpatra committed 2026-08-17 14:08:41 -04:00
1 parent 3d077409be
commit af89bfd0b1
4 files changed
+37 -13

No files matched your search

@@ -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]
@@ -47,12 +47,7 @@ public sealed class AppleDownloadStreamingCapabilityAdapter : IProviderStreaming
return ProviderOutcome<ProviderStreamLease>.Failure(new(ErrorFor(snapshot.State)));
if (!OutboundRequestGuard.TryCreateConfiguredServiceUri(settings.BaseUrl, out var baseUri, out _))
return ProviderOutcome<ProviderStreamLease>.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<ProviderStreamLease>.Success(new(
+17 -5
View File
@@ -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}")
@@ -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]: