feat(spotify): add spotify skill and oauth generator for openclaw
This commit is contained in:
@@ -46,6 +46,12 @@ Smart shopping advisor and Vorrats-Concierge.
|
||||
- **Features:** German supermarket & discounter weekly discounts (Edeka, Rewe, Aldi, Lidl, Kaufland, Netto, Penny, dm, Rossmann), Vorratskauf multi-brand comparison (Mutti / Oro di Parma), diaper piece-price calculator (ct/Windel), Geizhals price comparison & all-time-low price history, and eBay marketplace search.
|
||||
- **Tools:** Standalone MCP servers and CLIs at `scripts/supermarket_deals.py`, `scripts/geizhals_mcp.py`, and `scripts/ebay_mcp.py`.
|
||||
|
||||
### [spotify](skills/spotify/SKILL.md)
|
||||
Spotify music intelligence, catalog search, playback control, playlist curation, party queueing, and personal listening history.
|
||||
- **Features:** 35+ tools via `mcp-server-spotify` (search, playlists, player/queue, listening stats, top items).
|
||||
- **Synergy:** Bridges Spotify search with Home Assistant to stream Spotify URIs directly on Sonos multi-room speakers and sync Philips Hue lighting.
|
||||
- **Tools:** Includes one-time token generator `scripts/spotify_auth.py`.
|
||||
|
||||
### [skill-creator](skills/skill-creator/SKILL.md)
|
||||
Guidelines and scripts to help developers build high-quality, token-efficient Claude skills.
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: spotify
|
||||
description: Full Spotify music intelligence, catalog search, playback control, playlist curation, party queueing, and personal listening history. Use when the user asks to search for music/podcasts/audiobooks, control Spotify playback, queue tracks, check what is currently playing, create or curate playlists (including collaborative mixes for friends), or inspect listening history and top tracks.
|
||||
---
|
||||
|
||||
# 🎵 Spotify Skill for OpenClaw
|
||||
|
||||
Full integration with the **Spotify Web API** powered by `mcp-server-spotify`.
|
||||
|
||||
## 🛠️ MCP Tools & Capabilities
|
||||
|
||||
You have access to 35+ dedicated Spotify MCP tools:
|
||||
|
||||
### 1. Catalog Search
|
||||
- `search(query, types, limit)`: Search for tracks, artists, albums, playlists, shows, episodes, or audiobooks.
|
||||
- `types`: array containing `"track"`, `"artist"`, `"album"`, `"playlist"`, `"show"`, `"episode"`, `"audiobook"`.
|
||||
- Returns Spotify URIs (`spotify:track:...`), titles, artist names, album info, and web URLs (`https://open.spotify.com/...`).
|
||||
|
||||
### 2. Playback & Device Control
|
||||
- `get_playback_state()`: Returns current player status, active device, shuffle/repeat states, and track progress.
|
||||
- `get_currently_playing()`: Quick status of the currently playing track or episode.
|
||||
- `play(device_id, context_uri, uris, position_ms)`: Start or resume playback. Can play a specific playlist/album URI (`context_uri`) or a list of track URIs (`uris`).
|
||||
- `pause(device_id)`: Pause active playback.
|
||||
- `next_track(device_id)` / `previous_track(device_id)`: Skip tracks.
|
||||
- `seek(position_ms, device_id)`: Seek to a position in milliseconds.
|
||||
- `set_volume(volume_percent, device_id)`: Set volume (0-100).
|
||||
- `toggle_shuffle(state, device_id)`: Enable or disable shuffle.
|
||||
- `transfer_playback(device_ids, play)`: Transfer active playback to another Spotify Connect device.
|
||||
- `get_devices()`: List available Spotify Connect devices (TVs, laptops, speakers, mobile).
|
||||
- `add_to_queue(uri, device_id)`: Add a track/episode URI to the user's active playback queue.
|
||||
- `get_queue()`: Inspect currently playing item and upcoming queued items.
|
||||
|
||||
### 3. Playlists & Collaboration
|
||||
- `create_playlist(name, description, public)`: Create a new Spotify playlist for the user.
|
||||
- `get_playlist(playlist_id)`: Retrieve playlist metadata and owner.
|
||||
- `get_playlist_items(playlist_id, limit, offset)`: List tracks inside a playlist.
|
||||
- `add_playlist_items(playlist_id, uris, position)`: Add track URIs to an existing playlist (supports batch addition).
|
||||
- `remove_playlist_items(playlist_id, uris)`: Remove tracks from a playlist.
|
||||
- `reorder_playlist_items(playlist_id, range_start, insert_before)`: Reorder songs in a playlist.
|
||||
- `get_my_playlists(limit, offset)`: List current user's saved and created playlists.
|
||||
|
||||
### 4. Personal History, Taste & Social
|
||||
- `get_recently_played(limit)`: History of recently played tracks with timestamps.
|
||||
- `get_my_top_items(type, time_range, limit)`: Fetch top tracks or top artists over short (`"short_term"` = 4 weeks), medium (`"medium_term"` = 6 months), or long term (`"long_term"` = years).
|
||||
- `get_my_profile()`: User's profile details and country.
|
||||
- `whoami()`: Diagnostic helper returning authentication state, active device, and granted scopes.
|
||||
|
||||
---
|
||||
|
||||
## 📱 Telegram Presentation Guidelines
|
||||
|
||||
When formatting Spotify results or music recommendations for Telegram:
|
||||
- **Clean Markdown Links**: Always format track and playlist links nicely:
|
||||
- `▶️ [Track Title - Artist Name](https://open.spotify.com/track/...)`
|
||||
- `📁 [Playlist Name](https://open.spotify.com/playlist/...)`
|
||||
- **Current Playing Card**: When asked *"what's playing?"*:
|
||||
- Display: `🎶 *Track Title* by *Artist*\n💿 *Album Title* (Year)\n⏱️ mm:ss / mm:ss [■■■□□□□□□□]\n🔗 [Open in Spotify](url)`
|
||||
- **Friend Requests / Party Aux DJ**:
|
||||
- When a friend in a Telegram group asks to add a song:
|
||||
1. Search Spotify for the best match via `search`.
|
||||
2. Add to active queue (`add_to_queue`) or the group's party playlist (`add_playlist_items`).
|
||||
3. Confirm with a friendly message: *"Added 🎵 [Song - Artist](url) to the queue! Next up."*
|
||||
|
||||
---
|
||||
|
||||
## 🏡 Home Assistant (Sonos & Hue) Synergy
|
||||
|
||||
OpenClaw has access to both **Spotify MCP** and **Home Assistant MCP (`ha-mcp`)**:
|
||||
- **Spotify MCP is the Music Brain**: Use it to search the global catalog, find Spotify URIs, build playlists, and inspect listening history.
|
||||
- **Home Assistant is the Physical Hardware Controller**:
|
||||
- Home Assistant's `media_player.play_media` requires a Spotify URI (`spotify:track:...`, `spotify:playlist:...`).
|
||||
- When asked to *"Play X in the living room / on Sonos"*:
|
||||
1. Use `search` from Spotify MCP to get the exact `spotify:track:...` or `spotify:playlist:...` URI.
|
||||
2. Call `ha-mcp:ha_call_service`:
|
||||
- Domain: `media_player`
|
||||
- Service: `play_media`
|
||||
- Service Data: `{"media_content_id": "<spotify_uri>", "media_content_type": "music"}`
|
||||
- Target: `{"entity_id": "media_player.sonos_..."}`
|
||||
3. If asked for ambience or party lighting, call `ha-mcp:ha_call_service` on `light.turn_on` or activate Hue scenes simultaneously.
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Spotify OAuth Helper for OpenClaw
|
||||
Generates a permanent refresh_token for OpenClaw's Spotify MCP Server.
|
||||
|
||||
Usage:
|
||||
python3 spotify_auth.py --client-id <ID> --client-secret <SECRET>
|
||||
Or simply:
|
||||
python3 spotify_auth.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
DEFAULT_PORT = 8888
|
||||
DEFAULT_REDIRECT_URI = f"http://127.0.0.1:{DEFAULT_PORT}/callback"
|
||||
|
||||
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
|
||||
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
|
||||
SPOTIFY_ME_URL = "https://api.spotify.com/v1/me"
|
||||
|
||||
SCOPES = [
|
||||
# User profile & account
|
||||
"user-read-private",
|
||||
"user-read-email",
|
||||
# Playback & Devices
|
||||
"user-read-playback-state",
|
||||
"user-modify-playback-state",
|
||||
"user-read-currently-playing",
|
||||
"user-read-recently-played",
|
||||
# Library
|
||||
"user-library-read",
|
||||
"user-library-modify",
|
||||
# Top stats & history
|
||||
"user-top-read",
|
||||
"user-read-playback-position",
|
||||
# Playlists
|
||||
"playlist-read-private",
|
||||
"playlist-read-collaborative",
|
||||
"playlist-modify-public",
|
||||
"playlist-modify-private",
|
||||
# Follow
|
||||
"user-follow-read",
|
||||
"user-follow-modify",
|
||||
]
|
||||
|
||||
|
||||
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
||||
auth_code = None
|
||||
error = None
|
||||
|
||||
def do_GET(self):
|
||||
parsed_url = urllib.parse.urlparse(self.path)
|
||||
if parsed_url.path == "/callback":
|
||||
params = urllib.parse.parse_qs(parsed_url.query)
|
||||
if "code" in params:
|
||||
OAuthCallbackHandler.auth_code = params["code"][0]
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.end_headers()
|
||||
html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Spotify Auth Success</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #121212; color: #fff; text-align: center; }
|
||||
.card { background: #181818; padding: 40px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); border: 1px solid #282828; }
|
||||
h1 { color: #1DB954; margin-bottom: 12px; }
|
||||
p { color: #b3b3b3; font-size: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>✅ Authentication Successful!</h1>
|
||||
<p>OpenClaw has successfully connected to your Spotify account.</p>
|
||||
<p>You can now close this browser window and check your terminal.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
self.wfile.write(html.encode("utf-8"))
|
||||
else:
|
||||
err_msg = params.get("error", ["Unknown error"])[0]
|
||||
OAuthCallbackHandler.error = err_msg
|
||||
self.send_response(400)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(f"<h1>Authentication Failed</h1><p>{err_msg}</p>".encode("utf-8"))
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# Silence standard HTTP access logging
|
||||
pass
|
||||
|
||||
|
||||
def exchange_code_for_tokens(client_id: str, client_secret: str, code: str, redirect_uri: str) -> dict:
|
||||
auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
|
||||
data = urllib.parse.urlencode({
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": redirect_uri,
|
||||
}).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(
|
||||
SPOTIFY_TOKEN_URL,
|
||||
data=data,
|
||||
headers={
|
||||
"Authorization": f"Basic {auth_header}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def fetch_user_profile(access_token: str) -> dict:
|
||||
req = urllib.request.Request(
|
||||
SPOTIFY_ME_URL,
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Spotify OAuth Helper for OpenClaw")
|
||||
parser.add_argument("--client-id", help="Spotify Developer App Client ID")
|
||||
parser.add_argument("--client-secret", help="Spotify Developer App Client Secret")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Local callback port (default: {DEFAULT_PORT})")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("\n🎵 ===========================================")
|
||||
print(" OpenClaw Spotify OAuth Token Generator")
|
||||
print("===========================================\n")
|
||||
|
||||
client_id = args.client_id or os.environ.get("SPOTIFY_CLIENT_ID")
|
||||
client_secret = args.client_secret or os.environ.get("SPOTIFY_CLIENT_SECRET")
|
||||
|
||||
if not client_id:
|
||||
client_id = input("Enter Spotify Client ID: ").strip()
|
||||
if not client_secret:
|
||||
client_secret = input("Enter Spotify Client Secret: ").strip()
|
||||
|
||||
if not client_id or not client_secret:
|
||||
print("❌ Error: Client ID and Client Secret are required.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
redirect_uri = f"http://127.0.0.1:{args.port}/callback"
|
||||
|
||||
# Start local HTTP server for OAuth callback
|
||||
try:
|
||||
server = HTTPServer(("127.0.0.1", args.port), OAuthCallbackHandler)
|
||||
except OSError as e:
|
||||
print(f"❌ Error: Port {args.port} is already in use. Try specifying another port with --port.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Build auth URL
|
||||
auth_params = {
|
||||
"client_id": client_id,
|
||||
"response_type": "code",
|
||||
"redirect_uri": redirect_uri,
|
||||
"scope": " ".join(SCOPES),
|
||||
"show_dialog": "true",
|
||||
}
|
||||
auth_url = f"{SPOTIFY_AUTH_URL}?{urllib.parse.urlencode(auth_params)}"
|
||||
|
||||
print(f"\n🌐 Opening browser to authorize Spotify account...")
|
||||
print(f"If the browser doesn't open automatically, visit this URL:\n\n{auth_url}\n")
|
||||
webbrowser.open(auth_url)
|
||||
|
||||
print(f"Waiting for authorization on {redirect_uri} ...")
|
||||
while OAuthCallbackHandler.auth_code is None and OAuthCallbackHandler.error is None:
|
||||
server.handle_request()
|
||||
|
||||
if OAuthCallbackHandler.error:
|
||||
print(f"\n❌ Authorization rejected or failed: {OAuthCallbackHandler.error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
code = OAuthCallbackHandler.auth_code
|
||||
print("🔄 Exchanging authorization code for permanent tokens...")
|
||||
|
||||
try:
|
||||
token_data = exchange_code_for_tokens(client_id, client_secret, code, redirect_uri)
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"\n❌ Failed to exchange code for tokens: {e.read().decode('utf-8')}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
access_token = token_data.get("access_token")
|
||||
refresh_token = token_data.get("refresh_token")
|
||||
|
||||
# Fetch user profile to display confirmation
|
||||
user_name = "Spotify User"
|
||||
try:
|
||||
profile = fetch_user_profile(access_token)
|
||||
user_name = f"{profile.get('display_name', 'User')} ({profile.get('id', '')})"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("\n🎉 ===================================================")
|
||||
print(f" Successfully Authenticated as: {user_name}")
|
||||
print("===================================================\n")
|
||||
print("Add these lines to your k8s/openclaw/openclaw.secret.yaml:")
|
||||
print("---------------------------------------------------")
|
||||
print(f" spotify-client-id: \"{client_id}\"")
|
||||
print(f" spotify-client-secret: \"{client_secret}\"")
|
||||
print(f" spotify-refresh-token: \"{refresh_token}\"")
|
||||
print("---------------------------------------------------\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user