"""Record bare Spotify search results and render side-by-side comparisons.""" import json from pathlib import Path import httpx from scenario import Scenario TOKEN_URL = "https://accounts.spotify.com/api/token" SEARCH_URL = "https://api.spotify.com/v1/search" async def record_baselines( scenarios: tuple[Scenario, ...], client_id: str, client_secret: str, snapshot_root: Path, ) -> None: """Snapshot the top ten results from bare searches for every query.""" snapshot_root.mkdir(parents=True, exist_ok=True) async with httpx.AsyncClient(timeout=20.0) as client: token_response = await client.post( TOKEN_URL, data={"grant_type": "client_credentials"}, auth=(client_id, client_secret), ) token_response.raise_for_status() access_token = token_response.json()["access_token"] for scenario in scenarios: response = await client.get( SEARCH_URL, params={"q": scenario.query, "type": "track", "limit": 10}, headers={"Authorization": f"Bearer {access_token}"}, ) response.raise_for_status() tracks = response.json().get("tracks", {}).get("items", [])[:10] _write_json( snapshot_root / f"{scenario.key}.json", {"key": scenario.key, "query": scenario.query, "tracks": tracks}, ) def write_comparison( scenarios: tuple[Scenario, ...], snapshot_root: Path, report_root: Path, top_n: int, ) -> Path: """Write a markdown comparison for scenarios with both result arms.""" report_root.mkdir(parents=True, exist_ok=True) lines = ["# Pipeline and bare search comparison", ""] compared = 0 for scenario in scenarios: baseline_path = snapshot_root / f"{scenario.key}.json" pipeline_path = report_root / f"{scenario.key}.json" if not baseline_path.exists() or not pipeline_path.exists(): continue baseline = json.loads(baseline_path.read_text(encoding="ascii")) pipeline = json.loads(pipeline_path.read_text(encoding="ascii")) lines.extend(_comparison_section(scenario, pipeline, baseline, top_n)) compared += 1 if compared == 0: lines.extend(["No scenario has results from both arms yet.", ""]) output_path = report_root / "baseline-comparison.md" output_path.write_text("\n".join(lines), encoding="utf-8") return output_path def _comparison_section( scenario: Scenario, pipeline: object, baseline: object, top_n: int, ) -> list[str]: pipeline_tracks = pipeline.get("tracks", []) if isinstance(pipeline, dict) else [] baseline_tracks = baseline.get("tracks", []) if isinstance(baseline, dict) else [] lines = [f"## {scenario.chip}", "", "| Rank | Pipeline | Bare search |", "| ---: | --- | --- |"] for index in range(top_n): pipeline_label = _pipeline_label(pipeline_tracks, index) baseline_label = _spotify_label(baseline_tracks, index) lines.append(f"| {index + 1} | {pipeline_label} | {baseline_label} |") lines.append("") return lines def _pipeline_label(tracks: object, index: int) -> str: if not isinstance(tracks, list) or index >= len(tracks): return "" event = tracks[index] track = event.get("track") if isinstance(event, dict) else None return _track_label(track, "title") def _spotify_label(tracks: object, index: int) -> str: if not isinstance(tracks, list) or index >= len(tracks): return "" return _track_label(tracks[index], "name") def _track_label(track: object, title_key: str) -> str: if not isinstance(track, dict): return "" artists = track.get("artists", []) names = [ artist.get("name", "") if isinstance(artist, dict) else str(artist) for artist in artists ] return f"{track.get(title_key, '')} by {', '.join(names)}" def _write_json(path: Path, payload: object) -> None: path.write_text( json.dumps(payload, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="ascii", )