# BPM Enrichment Design **Date:** 2026-05-25 **Status:** Approved ## Goal Add a `bpm` field to every track object returned by the MCP server, sourced from Spotify's `/audio-features` endpoint. ## Scope Three tools return track objects and must be updated: - `get_playlist_tracks` - `list_saved_tracks` - `search_tracks` No new MCP tools. No changes to `server.py`. ## Architecture ### New helper: `_enrich_with_bpm(sp, tracks)` Location: `spotify_client.py` - Takes a `spotipy.Spotify` client and a list of already-parsed track dicts. - Extracts track IDs from each dict's `"id"` field. - Slices into chunks of 100 (API limit) and calls `sp.audio_features(ids)` once per chunk. - Matches results back by position and sets `track["bpm"]` to the rounded tempo float. - If the API returns `None` for a track (features unavailable), sets `bpm` to `null`. - Mutates the list in place; returns nothing. ### Changes to track parsers Each of the three functions adds `"id": track["id"]` to the dict it builds (currently absent from the output). After the full track list is assembled, each function calls `_enrich_with_bpm(sp, results)` before returning. ## Data Shape Before: ```json { "name": "oh baby", "artists": ["LCD Soundsystem"], "album": "american dream", "duration_ms": 349452, "duration": "5:49", "popularity": null, "uri": "spotify:track:53PkA8aXiwH4ppa0V0iO7o", "added_at": "2026-05-22T17:41:18Z" } ``` After: ```json { "name": "oh baby", "artists": ["LCD Soundsystem"], "album": "american dream", "duration_ms": 349452, "duration": "5:49", "popularity": null, "uri": "spotify:track:53PkA8aXiwH4ppa0V0iO7o", "id": "53PkA8aXiwH4ppa0V0iO7o", "added_at": "2026-05-22T17:41:18Z", "bpm": 128.0 } ``` ## Error Handling - `sp.audio_features()` may return `None` entries for tracks with no features (rare, e.g. local files, some podcasts). These get `bpm: null`. - Any exception from `sp.audio_features()` propagates naturally — same behaviour as the rest of the client. ## API Cost - `get_playlist_tracks` / `list_saved_tracks`: 1 extra API call per 100 tracks (batched). - `search_tracks`: at most 1 extra call (max 50 results per search).