183 lines
5.9 KiB
Markdown
183 lines
5.9 KiB
Markdown
# BPM Enrichment Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Add a `bpm` field (tempo from Spotify audio-features) to every track object returned by the MCP server.
|
|
|
|
**Architecture:** A private `_enrich_with_bpm(sp, tracks)` helper in `spotify_client.py` batch-fetches audio features for a list of parsed track dicts (100 IDs per API call) and adds a `bpm` field in place. Each of the three track-returning functions stores the track `id` during parsing, then calls this helper before returning.
|
|
|
|
**Tech Stack:** Python 3.11+, spotipy (`sp.audio_features()`), no new dependencies.
|
|
|
|
---
|
|
|
|
### Task 1: Add `_enrich_with_bpm` helper
|
|
|
|
**Files:**
|
|
- Modify: `spotify_client.py` — add helper after `_format_duration`
|
|
|
|
- [ ] **Step 1: Add the helper function**
|
|
|
|
In `spotify_client.py`, insert the following function immediately after `_format_duration` (around line 58):
|
|
|
|
```python
|
|
def _enrich_with_bpm(sp: spotipy.Spotify, tracks: list[dict]) -> None:
|
|
ids = [t["id"] for t in tracks]
|
|
for i in range(0, len(ids), 100):
|
|
chunk_ids = ids[i : i + 100]
|
|
features = sp.audio_features(chunk_ids)
|
|
for track, feat in zip(tracks[i : i + 100], features):
|
|
track["bpm"] = round(feat["tempo"], 1) if feat else None
|
|
```
|
|
|
|
- [ ] **Step 2: Verify syntax**
|
|
|
|
```bash
|
|
cd /Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-spotify
|
|
.venv/bin/python -c "import spotify_client; print('OK')"
|
|
```
|
|
|
|
Expected: `OK`
|
|
|
|
---
|
|
|
|
### Task 2: Add `id` field and BPM enrichment to `get_playlist_tracks`
|
|
|
|
**Files:**
|
|
- Modify: `spotify_client.py:get_playlist_tracks`
|
|
|
|
- [ ] **Step 1: Add `"id"` to the track dict and call the helper**
|
|
|
|
In `get_playlist_tracks`, add `"id": track["id"],` to the `results.append({...})` block (alongside `"uri"`), and add `_enrich_with_bpm(sp, results)` just before the `return` statement:
|
|
|
|
```python
|
|
results.append({
|
|
"name": track["name"],
|
|
"artists": [a["name"] for a in track["artists"]],
|
|
"album": track["album"]["name"],
|
|
"duration_ms": track["duration_ms"],
|
|
"duration": _format_duration(track["duration_ms"]),
|
|
"popularity": track.get("popularity"),
|
|
"id": track["id"],
|
|
"uri": track["uri"],
|
|
"added_at": item["added_at"],
|
|
})
|
|
page += 1
|
|
response = sp.next(response) if response["next"] else None
|
|
_enrich_with_bpm(sp, results)
|
|
_dbg(f"get_playlist_tracks returning {len(results)} tracks")
|
|
return results
|
|
```
|
|
|
|
- [ ] **Step 2: Smoke test**
|
|
|
|
```bash
|
|
SPOTIPY_CLIENT_ID=d66baed203d1461a860acbc5db27e3f5 \
|
|
SPOTIPY_CLIENT_SECRET=6d9ccf95957749ffac433919b585f4ff \
|
|
SPOTIPY_REDIRECT_URI=http://127.0.0.1:8888/callback \
|
|
.venv/bin/python -c "
|
|
import spotify_client, json
|
|
tracks = spotify_client.get_playlist_tracks('7LhqpCM88rPqDqxTLadLmf')
|
|
for t in tracks[:3]:
|
|
print(t['name'], '|', t.get('bpm'), 'bpm')
|
|
" 2>/dev/null
|
|
```
|
|
|
|
Expected: 3 lines like `oh baby | 128.0 bpm`
|
|
|
|
---
|
|
|
|
### Task 3: Add `id` field and BPM enrichment to `list_saved_tracks`
|
|
|
|
**Files:**
|
|
- Modify: `spotify_client.py:list_saved_tracks`
|
|
|
|
- [ ] **Step 1: Add `"id"` to the track dict and call the helper**
|
|
|
|
In `list_saved_tracks`, add `"id": track["id"],` to the `results.append({...})` block, and add `_enrich_with_bpm(sp, results)` just before the `return` statement:
|
|
|
|
```python
|
|
results.append({
|
|
"name": track["name"],
|
|
"artists": [a["name"] for a in track["artists"]],
|
|
"album": track["album"]["name"],
|
|
"duration_ms": track["duration_ms"],
|
|
"duration": _format_duration(track["duration_ms"]),
|
|
"popularity": track["popularity"],
|
|
"id": track["id"],
|
|
"uri": track["uri"],
|
|
"added_at": item["added_at"],
|
|
})
|
|
_enrich_with_bpm(sp, results)
|
|
return results
|
|
```
|
|
|
|
- [ ] **Step 2: Smoke test**
|
|
|
|
```bash
|
|
SPOTIPY_CLIENT_ID=d66baed203d1461a860acbc5db27e3f5 \
|
|
SPOTIPY_CLIENT_SECRET=6d9ccf95957749ffac433919b585f4ff \
|
|
SPOTIPY_REDIRECT_URI=http://127.0.0.1:8888/callback \
|
|
.venv/bin/python -c "
|
|
import spotify_client
|
|
tracks = spotify_client.list_saved_tracks(limit=3)
|
|
for t in tracks:
|
|
print(t['name'], '|', t.get('bpm'), 'bpm')
|
|
" 2>/dev/null
|
|
```
|
|
|
|
Expected: 3 lines with bpm values.
|
|
|
|
---
|
|
|
|
### Task 4: Add `id` field and BPM enrichment to `search_tracks`
|
|
|
|
**Files:**
|
|
- Modify: `spotify_client.py:search_tracks`
|
|
|
|
- [ ] **Step 1: Add `"id"` to the track dict and call the helper**
|
|
|
|
In `search_tracks`, add `"id": track["id"],` to the `results.append({...})` block, and add `_enrich_with_bpm(sp, results)` just before the `return` statement:
|
|
|
|
```python
|
|
for track in response["tracks"]["items"]:
|
|
results.append({
|
|
"name": track["name"],
|
|
"artists": [a["name"] for a in track["artists"]],
|
|
"album": track["album"]["name"],
|
|
"duration_ms": track["duration_ms"],
|
|
"duration": _format_duration(track["duration_ms"]),
|
|
"popularity": track["popularity"],
|
|
"id": track["id"],
|
|
"uri": track["uri"],
|
|
})
|
|
_enrich_with_bpm(sp, results)
|
|
return results
|
|
```
|
|
|
|
- [ ] **Step 2: Smoke test**
|
|
|
|
```bash
|
|
SPOTIPY_CLIENT_ID=d66baed203d1461a860acbc5db27e3f5 \
|
|
SPOTIPY_CLIENT_SECRET=6d9ccf95957749ffac433919b585f4ff \
|
|
SPOTIPY_REDIRECT_URI=http://127.0.0.1:8888/callback \
|
|
.venv/bin/python -c "
|
|
import spotify_client
|
|
tracks = spotify_client.search_tracks('LCD Soundsystem', limit=3)
|
|
for t in tracks:
|
|
print(t['name'], '|', t.get('bpm'), 'bpm')
|
|
" 2>/dev/null
|
|
```
|
|
|
|
Expected: 3 lines with bpm values.
|
|
|
|
---
|
|
|
|
### Task 5: Commit
|
|
|
|
- [ ] **Commit all changes**
|
|
|
|
```bash
|
|
git add spotify_client.py
|
|
git commit -m "feat: add bpm field to all track-returning tools via audio-features endpoint"
|
|
```
|