feat: add key-free demo mode replaying recorded fixtures
This commit is contained in:
parent
0664cc2d27
commit
401a0ddea7
21 changed files with 1040 additions and 31 deletions
|
|
@ -1,4 +1,4 @@
|
|||
"""Authenticated recommendation streaming and playlist creation routes."""
|
||||
"""Recommendation streaming and playlist creation routes."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable
|
||||
|
|
@ -38,6 +38,10 @@ from app.pipeline.orchestrator import RecommendationPipeline
|
|||
from app.ports.protocols import MusicCatalog, PlaylistWriter
|
||||
|
||||
SpotifyClientFactory = Callable[[SpotifySession], MusicCatalog | PlaylistWriter]
|
||||
MusicCatalogFactory = Callable[
|
||||
[SpotifySession, str, tuple[PreviousRecommendation, ...]],
|
||||
MusicCatalog,
|
||||
]
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -47,14 +51,12 @@ async def recommendations(
|
|||
request: Request,
|
||||
payload: RecommendationRequest,
|
||||
) -> StreamingResponse:
|
||||
"""Stream one authenticated discovery response as NDJSON."""
|
||||
"""Stream one session-resolved discovery response as NDJSON."""
|
||||
resolved = resolve_session(request)
|
||||
if resolved is None:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
||||
pipeline = cast(RecommendationPipeline, request.app.state.recommendation_pipeline)
|
||||
factory = cast(SpotifyClientFactory, request.app.state.spotify_client_factory)
|
||||
catalog = cast(MusicCatalog, factory(resolved.session))
|
||||
request_id = cast(str, request.state.request_id)
|
||||
history = tuple(ConversationTurn(turn.role, turn.content) for turn in payload.history)
|
||||
previous = tuple(
|
||||
|
|
@ -66,6 +68,8 @@ async def recommendations(
|
|||
)
|
||||
for item in payload.prior_recommendations
|
||||
)
|
||||
factory = cast(MusicCatalogFactory, request.app.state.music_catalog_factory)
|
||||
catalog = factory(resolved.session, payload.query, previous)
|
||||
|
||||
return StreamingResponse(
|
||||
_stream_lines(
|
||||
|
|
@ -86,7 +90,7 @@ async def create_playlist(
|
|||
request: Request,
|
||||
payload: PlaylistCreateRequest,
|
||||
) -> PlaylistCreateResponse:
|
||||
"""Create and fill one authenticated Spotify playlist."""
|
||||
"""Create and fill one live or simulated playlist."""
|
||||
resolved = resolve_session(request)
|
||||
if resolved is None:
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""HTTP routes for Spotify login and session management."""
|
||||
"""HTTP routes for login and session management."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
|
@ -9,7 +9,7 @@ from fastapi.responses import JSONResponse, RedirectResponse, Response
|
|||
|
||||
from app.adapters.spotify.login import begin_login, complete_login
|
||||
from app.adapters.spotify.session import PendingLogins, SessionStore, SpotifySession
|
||||
from app.config import Settings
|
||||
from app.config import AppMode, Settings
|
||||
|
||||
SESSION_COOKIE_NAME = "discovery_session"
|
||||
|
||||
|
|
@ -26,8 +26,10 @@ class ResolvedSession:
|
|||
|
||||
@router.get("/api/auth/login")
|
||||
def login(request: Request) -> RedirectResponse:
|
||||
"""Start Spotify Authorization Code with PKCE login."""
|
||||
"""Start the configured live or demo login flow."""
|
||||
application_settings = cast(Settings, request.app.state.settings)
|
||||
if application_settings.app_mode is AppMode.DEMO:
|
||||
return RedirectResponse("/?login=demo", status_code=307)
|
||||
pending_logins = cast(PendingLogins, request.app.state.pending_logins)
|
||||
authorize_url = begin_login(application_settings, pending_logins)
|
||||
return RedirectResponse(authorize_url, status_code=307)
|
||||
|
|
@ -40,11 +42,13 @@ async def callback(
|
|||
state: str | None = None,
|
||||
error: str | None = None,
|
||||
) -> RedirectResponse:
|
||||
"""Complete Spotify login and establish an opaque cookie session."""
|
||||
"""Complete live login and establish an opaque cookie session."""
|
||||
application_settings = cast(Settings, request.app.state.settings)
|
||||
if application_settings.app_mode is AppMode.DEMO:
|
||||
return RedirectResponse("/?login=demo", status_code=307)
|
||||
if error is not None or code is None or state is None:
|
||||
return _login_error_redirect()
|
||||
|
||||
application_settings = cast(Settings, request.app.state.settings)
|
||||
http = cast(httpx2.AsyncClient, request.app.state.http)
|
||||
pending_logins = cast(PendingLogins, request.app.state.pending_logins)
|
||||
session_store = cast(SessionStore, request.app.state.session_store)
|
||||
|
|
@ -80,6 +84,12 @@ def current_session(request: Request) -> JSONResponse:
|
|||
return JSONResponse({"display_name": resolved.session.display_name})
|
||||
|
||||
|
||||
@router.get("/api/suggestions")
|
||||
def suggestions(request: Request) -> list[dict[str, str]]:
|
||||
"""Return shared non-refinement suggestion chips and queries."""
|
||||
return cast(list[dict[str, str]], request.app.state.suggestions)
|
||||
|
||||
|
||||
@router.post("/api/auth/logout", status_code=204)
|
||||
def logout(request: Request) -> Response:
|
||||
"""Remove the current application session and clear its cookie."""
|
||||
|
|
@ -105,7 +115,11 @@ def _login_error_redirect() -> RedirectResponse:
|
|||
|
||||
|
||||
def resolve_session(request: Request) -> ResolvedSession | None:
|
||||
"""Resolve the cookie session or the installed live seed session."""
|
||||
"""Resolve the stable demo identity, cookie, or live seed session."""
|
||||
application_settings = cast(Settings, request.app.state.settings)
|
||||
if application_settings.app_mode is AppMode.DEMO:
|
||||
demo_session = cast(SpotifySession, request.app.state.demo_session)
|
||||
return ResolvedSession("demo", demo_session)
|
||||
session_store = cast(SessionStore, request.app.state.session_store)
|
||||
cookie_session_id = request.cookies.get(SESSION_COOKIE_NAME)
|
||||
if cookie_session_id is not None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue