feat: terminal spotify client (ncmpcpp-style)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import requests
|
||||
from typing import Optional
|
||||
|
||||
BASE_URL = "https://api.spotify.com/v1"
|
||||
|
||||
|
||||
class SpotifyClient:
|
||||
def __init__(self, token: str):
|
||||
self.token = token
|
||||
self.headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
def _get(self, endpoint, params=None):
|
||||
resp = requests.get(f"{BASE_URL}{endpoint}", headers=self.headers, params=params)
|
||||
if resp.status_code == 401:
|
||||
raise TokenExpired()
|
||||
if resp.status_code == 429:
|
||||
import time
|
||||
retry = int(resp.headers.get("Retry-After", 1))
|
||||
time.sleep(retry)
|
||||
return self._get(endpoint, params)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _put(self, endpoint, data=None):
|
||||
resp = requests.put(f"{BASE_URL}{endpoint}", headers=self.headers, json=data)
|
||||
if resp.status_code == 401:
|
||||
raise TokenExpired()
|
||||
if resp.status_code == 429:
|
||||
import time
|
||||
retry = int(resp.headers.get("Retry-After", 1))
|
||||
time.sleep(retry)
|
||||
return self._put(endpoint, data)
|
||||
if resp.status_code == 204:
|
||||
return None
|
||||
resp.raise_for_status()
|
||||
return resp.json() if resp.text else None
|
||||
|
||||
def _post(self, endpoint, data=None):
|
||||
resp = requests.post(f"{BASE_URL}{endpoint}", headers=self.headers, json=data)
|
||||
if resp.status_code == 401:
|
||||
raise TokenExpired()
|
||||
if resp.status_code == 429:
|
||||
import time
|
||||
retry = int(resp.headers.get("Retry-After", 1))
|
||||
time.sleep(retry)
|
||||
return self._post(endpoint, data)
|
||||
if resp.status_code == 204:
|
||||
return None
|
||||
resp.raise_for_status()
|
||||
return resp.json() if resp.text else None
|
||||
|
||||
def _delete(self, endpoint, data=None):
|
||||
resp = requests.delete(f"{BASE_URL}{endpoint}", headers=self.headers, json=data)
|
||||
if resp.status_code == 401:
|
||||
raise TokenExpired()
|
||||
if resp.status_code == 429:
|
||||
import time
|
||||
retry = int(resp.headers.get("Retry-After", 1))
|
||||
time.sleep(retry)
|
||||
return self._delete(endpoint, data)
|
||||
if resp.status_code == 204:
|
||||
return None
|
||||
resp.raise_for_status()
|
||||
return resp.json() if resp.text else None
|
||||
|
||||
# --- Playback ---
|
||||
def get_playback(self):
|
||||
return self._get("/me/player")
|
||||
|
||||
def get_current_track(self):
|
||||
return self._get("/me/player/currently-playing")
|
||||
|
||||
def play(self, context_uri=None, uris=None, offset=None, position_ms=None):
|
||||
data = {}
|
||||
if context_uri:
|
||||
data["context_uri"] = context_uri
|
||||
if uris:
|
||||
data["uris"] = uris
|
||||
if offset:
|
||||
data["offset"] = offset
|
||||
if position_ms is not None:
|
||||
data["position_ms"] = position_ms
|
||||
return self._put("/me/player/play", data)
|
||||
|
||||
def pause(self):
|
||||
return self._put("/me/player/pause")
|
||||
|
||||
def next_track(self):
|
||||
return self._post("/me/player/next")
|
||||
|
||||
def previous_track(self):
|
||||
return self._post("/me/player/previous")
|
||||
|
||||
def seek(self, position_ms):
|
||||
return self._put(f"/me/player/seek?position_ms={position_ms}")
|
||||
|
||||
def set_volume(self, volume):
|
||||
return self._put(f"/me/player/volume?volume={volume}")
|
||||
|
||||
def set_repeat(self, state):
|
||||
return self._put(f"/me/player/repeat?state={state}")
|
||||
|
||||
def set_shuffle(self, state):
|
||||
return self._put(f"/me/player/shuffle?state={state}")
|
||||
|
||||
def transfer_playback(self, device_id, force_play=True):
|
||||
return self._put("/me/player", {
|
||||
"device_ids": [device_id],
|
||||
"play": force_play,
|
||||
})
|
||||
|
||||
def get_devices(self):
|
||||
return self._get("/me/player/devices")
|
||||
|
||||
# --- Player Info ---
|
||||
def get_recently_played(self, limit=20):
|
||||
return self._get("/me/player/recently-played", {"limit": limit})
|
||||
|
||||
def get_queue(self):
|
||||
return self._get("/me/player/queue")
|
||||
|
||||
def add_to_queue(self, uri):
|
||||
return self._post(f"/me/player/queue?uri={uri}")
|
||||
|
||||
# --- Library ---
|
||||
def get_saved_tracks(self, limit=50, offset=0):
|
||||
return self._get("/me/tracks", {"limit": limit, "offset": offset})
|
||||
|
||||
def get_saved_albums(self, limit=50, offset=0):
|
||||
return self._get("/me/albums", {"limit": limit, "offset": offset})
|
||||
|
||||
def get_saved_artists(self, limit=50, offset=0):
|
||||
return self._get("/me/following", {"type": "artist", "limit": limit, "offset": offset})
|
||||
|
||||
def save_track(self, track_ids):
|
||||
return self._put("/me/tracks", {"ids": track_ids})
|
||||
|
||||
def remove_saved_track(self, track_ids):
|
||||
return self._delete("/me/tracks", {"ids": track_ids})
|
||||
|
||||
def check_saved_track(self, track_ids):
|
||||
return self._get(f"/me/tracks/contains?ids={','.join(track_ids)}")
|
||||
|
||||
# --- Playlists ---
|
||||
def get_playlists(self, limit=50, offset=0):
|
||||
return self._get("/me/playlists", {"limit": limit, "offset": offset})
|
||||
|
||||
def get_playlist(self, playlist_id):
|
||||
return self._get(f"/playlists/{playlist_id}")
|
||||
|
||||
def get_playlist_tracks(self, playlist_id, limit=100, offset=0):
|
||||
return self._get(f"/playlists/{playlist_id}/tracks", {"limit": limit, "offset": offset})
|
||||
|
||||
def create_playlist(self, name, description="", public=False):
|
||||
user = self.get_current_user()
|
||||
return self._post(f"/users/{user['id']}/playlists", {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"public": public,
|
||||
})
|
||||
|
||||
def add_to_playlist(self, playlist_id, uris):
|
||||
return self._post(f"/playlists/{playlist_id}/tracks", {"uris": uris})
|
||||
|
||||
def remove_from_playlist(self, playlist_id, track_ids):
|
||||
return self._delete(f"/playlists/{playlist_id}/tracks", {
|
||||
"tracks": [{"id": tid} for tid in track_ids],
|
||||
})
|
||||
|
||||
# --- Albums ---
|
||||
def get_album(self, album_id):
|
||||
return self._get(f"/albums/{album_id}")
|
||||
|
||||
def get_album_tracks(self, album_id, limit=50, offset=0):
|
||||
return self._get(f"/albums/{album_id}/tracks", {"limit": limit, "offset": offset})
|
||||
|
||||
# --- Artists ---
|
||||
def get_artist(self, artist_id):
|
||||
return self._get(f"/artists/{artist_id}")
|
||||
|
||||
def get_artist_top_tracks(self, artist_id, country="US"):
|
||||
return self._get(f"/artists/{artist_id}/top-tracks", {"country": country})
|
||||
|
||||
def get_artist_albums(self, artist_id, limit=50, offset=0):
|
||||
return self._get(f"/artists/{artist_id}/albums", {
|
||||
"limit": limit, "offset": offset, "include_groups": "album,single"
|
||||
})
|
||||
|
||||
# --- Search ---
|
||||
def search(self, query, types="track,album,artist,playlist", limit=20, offset=0):
|
||||
return self._get("/search", {
|
||||
"q": query, "type": types, "limit": limit, "offset": offset
|
||||
})
|
||||
|
||||
# --- User ---
|
||||
def get_current_user(self):
|
||||
return self._get("/me")
|
||||
|
||||
|
||||
class TokenExpired(Exception):
|
||||
pass
|
||||
Reference in New Issue
Block a user