nexusbert commited on
Commit
750f074
·
1 Parent(s): 0cb016f

Add Spotify player and user recommendation APIs with PUT and GET wrappers for playback control and user track recommendations

Browse files
app/spotify_http.py CHANGED
@@ -135,4 +135,43 @@ def spotify_post(
135
  time.sleep(sleep_s)
136
 
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
 
135
  time.sleep(sleep_s)
136
 
137
 
138
+ def spotify_put(
139
+ url: str,
140
+ access_token: str,
141
+ *,
142
+ params: Optional[Dict[str, Any]] = None,
143
+ json: Optional[Dict[str, Any]] = None,
144
+ data: Optional[Dict[str, Any]] = None,
145
+ timeout: int = 20,
146
+ ) -> Dict[str, Any]:
147
+ """
148
+ PUT wrapper with basic retry/backoff for rate limits and transient upstream errors.
149
+ """
150
+ max_retries = 2
151
+ attempt = 0
152
+ while True:
153
+ resp = requests.put(
154
+ url,
155
+ headers={"Authorization": f"Bearer {access_token}"},
156
+ params=params,
157
+ json=json,
158
+ data=data,
159
+ timeout=timeout,
160
+ )
161
+ if resp.status_code < 400:
162
+ if resp.status_code == 204 or not resp.content:
163
+ return {}
164
+ return resp.json()
165
+
166
+ err = _parse_spotify_error(resp)
167
+ retryable = err.status_code in {429, 502, 503}
168
+ if not retryable or attempt >= max_retries:
169
+ raise err
170
+
171
+ attempt += 1
172
+ if err.status_code == 429 and err.retry_after is not None:
173
+ sleep_s = err.retry_after
174
+ else:
175
+ sleep_s = min(8.0, (2.0 ** (attempt - 1))) + random.uniform(0.0, 0.25)
176
+ time.sleep(sleep_s)
177
 
app/spotify_player_api.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .spotify_http import spotify_get, spotify_post, spotify_put
6
+
7
+
8
+ def get_playback_state(
9
+ access_token: str,
10
+ *,
11
+ market: Optional[str] = None,
12
+ additional_types: Optional[str] = None,
13
+ ) -> Dict[str, Any]:
14
+ params: Dict[str, Any] = {}
15
+ if market:
16
+ params["market"] = market
17
+ if additional_types:
18
+ params["additional_types"] = additional_types
19
+ return spotify_get("https://api.spotify.com/v1/me/player", access_token, params=params)
20
+
21
+
22
+ def get_available_devices(access_token: str) -> Dict[str, Any]:
23
+ return spotify_get("https://api.spotify.com/v1/me/player/devices", access_token)
24
+
25
+
26
+ def get_currently_playing(
27
+ access_token: str,
28
+ *,
29
+ market: Optional[str] = None,
30
+ additional_types: Optional[str] = None,
31
+ ) -> Dict[str, Any]:
32
+ params: Dict[str, Any] = {}
33
+ if market:
34
+ params["market"] = market
35
+ if additional_types:
36
+ params["additional_types"] = additional_types
37
+ return spotify_get("https://api.spotify.com/v1/me/player/currently-playing", access_token, params=params)
38
+
39
+
40
+ def start_or_resume_playback(
41
+ access_token: str,
42
+ *,
43
+ device_id: Optional[str] = None,
44
+ context_uri: Optional[str] = None,
45
+ uris: Optional[list[str]] = None,
46
+ offset: Optional[Dict[str, Any]] = None,
47
+ position_ms: Optional[int] = None,
48
+ ) -> Dict[str, Any]:
49
+ params: Dict[str, Any] = {}
50
+ if device_id:
51
+ params["device_id"] = device_id
52
+ body: Dict[str, Any] = {}
53
+ if context_uri:
54
+ body["context_uri"] = context_uri
55
+ if uris:
56
+ body["uris"] = uris
57
+ if offset is not None:
58
+ body["offset"] = offset
59
+ if position_ms is not None:
60
+ body["position_ms"] = position_ms
61
+ return spotify_put("https://api.spotify.com/v1/me/player/play", access_token, params=params, json=body)
62
+
63
+
64
+ def pause_playback(access_token: str, *, device_id: Optional[str] = None) -> Dict[str, Any]:
65
+ params: Dict[str, Any] = {}
66
+ if device_id:
67
+ params["device_id"] = device_id
68
+ return spotify_put("https://api.spotify.com/v1/me/player/pause", access_token, params=params)
69
+
70
+
71
+ def skip_next(access_token: str, *, device_id: Optional[str] = None) -> Dict[str, Any]:
72
+ params: Dict[str, Any] = {}
73
+ if device_id:
74
+ params["device_id"] = device_id
75
+ return spotify_post("https://api.spotify.com/v1/me/player/next", access_token, params=params)
76
+
77
+
78
+ def skip_previous(access_token: str, *, device_id: Optional[str] = None) -> Dict[str, Any]:
79
+ params: Dict[str, Any] = {}
80
+ if device_id:
81
+ params["device_id"] = device_id
82
+ return spotify_post("https://api.spotify.com/v1/me/player/previous", access_token, params=params)
83
+
84
+
85
+ def transfer_playback(
86
+ access_token: str,
87
+ *,
88
+ device_ids: list[str],
89
+ play: Optional[bool] = None,
90
+ ) -> Dict[str, Any]:
91
+ body: Dict[str, Any] = {"device_ids": device_ids}
92
+ if play is not None:
93
+ body["play"] = play
94
+ return spotify_put("https://api.spotify.com/v1/me/player", access_token, json=body)
95
+
96
+
97
+ def add_to_queue(
98
+ access_token: str,
99
+ *,
100
+ uri: str,
101
+ device_id: Optional[str] = None,
102
+ ) -> Dict[str, Any]:
103
+ params: Dict[str, Any] = {"uri": uri}
104
+ if device_id:
105
+ params["device_id"] = device_id
106
+ return spotify_post("https://api.spotify.com/v1/me/player/queue", access_token, params=params)
107
+
108
+
109
+
app/spotify_user_reco.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from typing import Any, Dict, Optional
5
+
6
+ from .config import get_settings
7
+ from .spotify_http import spotify_get
8
+ from .spotify_client import recommend_track_for_emotion
9
+
10
+
11
+ settings = get_settings()
12
+
13
+
14
+ def _get_user_top_tracks(access_token: str, limit: int = 20) -> list[Dict[str, Any]]:
15
+ data = spotify_get(
16
+ "https://api.spotify.com/v1/me/top/tracks",
17
+ access_token,
18
+ params={"limit": limit, "time_range": "medium_term"},
19
+ )
20
+ return (data.get("items") or [])[:limit]
21
+
22
+
23
+ def _get_recently_played(access_token: str, limit: int = 20) -> list[Dict[str, Any]]:
24
+ data = spotify_get(
25
+ "https://api.spotify.com/v1/me/player/recently-played",
26
+ access_token,
27
+ params={"limit": limit},
28
+ )
29
+ items = data.get("items") or []
30
+ out: list[Dict[str, Any]] = []
31
+ for it in items:
32
+ t = (it or {}).get("track") or {}
33
+ if not t:
34
+ continue
35
+ out.append(t)
36
+ return out[:limit]
37
+
38
+
39
+ def recommend_user_track_for_emotion(
40
+ access_token: str,
41
+ emotion: str,
42
+ *,
43
+ market: Optional[str] = None,
44
+ ) -> Dict[str, Any]:
45
+ mk = market or settings.spotify_market or "NG"
46
+ emotion_norm = (emotion or "").lower()
47
+
48
+ try:
49
+ top_tracks = _get_user_top_tracks(access_token, limit=15)
50
+ except Exception:
51
+ top_tracks = []
52
+
53
+ try:
54
+ recent_tracks = _get_recently_played(access_token, limit=15)
55
+ except Exception:
56
+ recent_tracks = []
57
+
58
+ seeds = top_tracks + recent_tracks
59
+ if not seeds:
60
+ return recommend_track_for_emotion(emotion, source="user")
61
+
62
+ artist_names: list[str] = []
63
+ track_names: list[str] = []
64
+
65
+ for t in seeds:
66
+ for a in t.get("artists") or []:
67
+ name = a.get("name")
68
+ if isinstance(name, str):
69
+ artist_names.append(name)
70
+ name = t.get("name")
71
+ if isinstance(name, str):
72
+ track_names.append(name)
73
+
74
+ artist_names = list(dict.fromkeys(artist_names))[:10]
75
+ track_names = list(dict.fromkeys(track_names))[:10]
76
+
77
+ queries: list[str] = []
78
+
79
+ for name in artist_names[:5]:
80
+ queries.append(f"{emotion_norm} {name}")
81
+ queries.append(f"{name}")
82
+
83
+ for title in track_names[:5]:
84
+ queries.append(f"{emotion_norm} {title}")
85
+
86
+ queries.append(emotion_norm)
87
+
88
+ items: list[Dict[str, Any]] = []
89
+ for q in queries:
90
+ data = spotify_get(
91
+ "https://api.spotify.com/v1/search",
92
+ access_token,
93
+ params={"q": q, "type": "track", "limit": 10, "market": mk},
94
+ )
95
+ batch = (data.get("tracks") or {}).get("items") or []
96
+ if batch:
97
+ items.extend(batch)
98
+ if len(items) >= 30:
99
+ break
100
+
101
+ if not items:
102
+ return recommend_track_for_emotion(emotion, source="user")
103
+
104
+ track = random.choice(items)
105
+ return {
106
+ "id": track.get("id"),
107
+ "name": track.get("name"),
108
+ "artists": [a.get("name") for a in track.get("artists", [])],
109
+ "preview_url": track.get("preview_url"),
110
+ "external_url": (track.get("external_urls") or {}).get("spotify"),
111
+ }
112
+
113
+