arcade-mcp/toolkits/spotify/arcade_spotify/tools/player.py
Nate Barbettini 799d376ae5
SDK: Generic OAuth 2.0 connector (#81)
- Implements https://app.clickup.com/t/86b1whxb3 on the SDK side
- - Corresponding Engine PR:
https://github.com/ArcadeAI/Engine/pull/113/files?w=1
- Updates existing toolkits with new syntax.
2024-10-03 16:40:02 -07:00

163 lines
5.5 KiB
Python

from typing import Annotated, Optional
import httpx
from arcade.core.errors import ToolExecutionError
from arcade.core.schema import ToolContext
from arcade.sdk import tool
from arcade.sdk.auth import Spotify
SPOTIFY_BASE_URL = "https://api.spotify.com/v1"
async def _send_spotify_request(
context: ToolContext,
method: str,
endpoint: str,
params: dict | None = None,
json_data: dict | None = None,
) -> httpx.Response:
"""
Send an asynchronous request to the Spotify API.
Args:
context: The tool context containing the authorization token.
method: The HTTP method (GET, POST, PUT, DELETE, etc.).
endpoint: The API endpoint path (e.g., "/me/player/play").
params: Query parameters to include in the request.
json_data: JSON data to include in the request body.
Returns:
The response object from the API request.
Raises:
ToolExecutionError: If the request fails for any reason.
"""
url = f"{SPOTIFY_BASE_URL}{endpoint}"
headers = {"Authorization": f"Bearer {context.authorization.token}"}
async with httpx.AsyncClient() as client:
try:
response = await client.request(
method, url, headers=headers, params=params, json=json_data
)
response.raise_for_status()
except httpx.RequestError as e:
raise ToolExecutionError(f"Failed to send request to Spotify API: {e}")
return response
def _handle_spotify_api_error(response: httpx.Response):
"""
Handle errors from the Spotify API by mapping common status codes to ToolExecutionErrors.
Args:
response: The response object from the API request.
Raises:
ToolExecutionError: If the response contains an error status code.
"""
status_code_map = {
401: ToolExecutionError("Unauthorized: Invalid or expired token"),
403: ToolExecutionError("Forbidden: User does not have Spotify Premium"),
429: ToolExecutionError("Too Many Requests: Rate limit exceeded"),
}
if response.status_code in status_code_map:
raise status_code_map[response.status_code]
elif response.status_code >= 400:
raise ToolExecutionError(f"Error: {response.status_code} - {response.text}")
@tool(
requires_auth=Spotify(
scopes=["user-modify-playback-state"],
)
)
async def pause(
context: ToolContext,
device_id: Annotated[
Optional[str],
"The id of the device this command is targeting. If omitted, the active device is targeted.",
] = None,
) -> Annotated[str, "Success string confirming the pause"]:
"""Pause the current track"""
endpoint = "/me/player/pause"
params = {"device_id": device_id} if device_id else {}
response = await _send_spotify_request(context, "PUT", endpoint, params=params)
if response.status_code >= 200 and response.status_code < 300:
return "Playback paused"
else:
_handle_spotify_api_error(response)
@tool(
requires_auth=Spotify(
scopes=["user-modify-playback-state"],
)
)
async def resume(
context: ToolContext,
device_id: Annotated[
Optional[str],
"The id of the device this command is targeting. If omitted, the active device is targeted.",
] = None,
) -> Annotated[str, "Success string confirming the playback resume"]:
"""Resume the current track, if any"""
endpoint = "/me/player/play"
params = {"device_id": device_id} if device_id else {}
response = await _send_spotify_request(context, "PUT", endpoint, params=params)
if response.status_code >= 200 and response.status_code < 300:
return "Playback resumed"
else:
_handle_spotify_api_error(response)
@tool(
requires_auth=Spotify(
scopes=["user-read-playback-state"],
)
)
async def get_playback_state(
context: ToolContext,
) -> Annotated[dict, "Information about the user's current playback state"]:
"""Get information about the user's current playback state, including track or episode, progress, and active device."""
endpoint = "/me/player"
response = await _send_spotify_request(context, "GET", endpoint)
if response.status_code == 204:
return {"status": "Playback not available or active"}
elif response.status_code == 200:
data = response.json()
# TODO: Return a more structured model
result = {
"device_name": data.get("device", {}).get("name"),
"currently_playing_type": data.get("currently_playing_type"),
}
if data.get("currently_playing_type") == "track":
item = data.get("item", {})
album = item.get("album", {})
result.update({
"album_name": album.get("name"),
"album_artists": [artist.get("name") for artist in album.get("artists", [])],
"album_spotify_url": album.get("external_urls", {}).get("spotify"),
"track_name": item.get("name"),
"track_artists": [artist.get("name") for artist in item.get("artists", [])],
})
elif data.get("currently_playing_type") == "episode":
item = data.get("item", {})
show = item.get("show", {})
result.update({
"show_name": show.get("name"),
"show_spotify_url": show.get("external_urls", {}).get("spotify"),
"episode_name": item.get("name"),
"episode_spotify_url": item.get("external_urls", {}).get("spotify"),
})
return result
else:
_handle_spotify_api_error(response)