Skill: braydoncoyer/spotify-history (version 1.1.0)

--- name: spotify-history description: Access Spotify listening history, top artists/tracks, and get personalized recommendations via the Spotify Web API. Use when fetching a user's recent plays, analyzing music taste, or generating recommendations. Requires one-time OAuth setup. --- # Spotify History & Recommendations Access Spotify listening history and get personalized recommendations. ## Setup (One-Time) ### Quick Setup (Recommended) Run the setup wizard: ```bash bash skills/spotify-history/scripts/setup.sh ``` This guides you through: 1. Creating a Spotify Developer App 2. Saving credentials securely 3. Authorizing access ### Manual Setup 1. **Create Spotify Developer App** - Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard) - Click **Create App** - Fill in: - **App name:** `Clawd` (or any name) - **App description:** `Personal assistant integration` - **Redirect URI:** `http://127.0.0.1:8888/callback` ⚠️ Use exact URL! - Save and copy **Client ID** and **Client Secret** 2. **Store Credentials** **Option A: Credentials file (recommended)** ```bash mkdir -p credentials cat > credentials/spotify.json <<EOF { "client_id": "your_client_id", "client_secret": "your_client_secret" } EOF chmod 600 credentials/spotify.json ``` **Option B: Environment variables** ```bash # Add to ~/.zshrc or ~/.bashrc export SPOTIFY_CLIENT_ID="your_client_id" export SPOTIFY_CLIENT_SECRET="your_client_secret" ``` 3. **Authenticate** **With browser (local machine):** ```bash python3 scripts/spotify-auth.py ``` **Headless (no browser):** ```bash python3 scripts/spotify-auth.py --headless ``` Follow the prompts to authorize via URL and paste the callback. Tokens are saved to `~/.config/spotify-clawd/token.json` and auto-refresh when expired. ## Usage ### Command Line ```bash # Recent listening history python3 scripts/spotify-api.py recent # Top artists (time_range: short_term, medium_term, long_term) python3 scripts/spotify-api.py top-artists medium_term # Top tracks python3 scripts/spotify-api.py top-tracks medium_term # Get recommendations based on your top artists python3 scripts/spotify-api.py recommend # Raw API call (any endpoint) python3 scripts/spotify-api.py json /me python3 scripts/spotify-api.py json /me/player/recently-played ``` ### Time Ranges - `short_term` — approximately last 4 weeks - `medium_term` — approximately last 6 months (default) - `long_term` — all time ### Example Output ``` Top Artists (medium_term): 1. Hans Zimmer [soundtrack, score] 2. John Williams [soundtrack, score] 3. Michael Giacchino [soundtrack, score] 4. Max Richter [ambient, modern classical] 5. Ludovico Einaudi [italian contemporary classical] ``` ## Agent Usage When user asks about music: - "What have I been listening to?" → `spotify-api.py recent` - "Who are my top artists?" → `spotify-api.py top-artists` - "Recommend new music" → `spotify-api.py recommend` + add your own knowledge For recommendations, combine API data with music knowledge to suggest similar artists not in their library. ## Troubleshooting ### "Spotify credentials not found!" - Make sure `credentials/spotify.json` exists **or** environment variables are set - Credential file is checked first, then env vars - Run `bash skills/spotify-history/scripts/setup.sh` to create credentials ### "Not authenticated. Run spotify-auth.py first." - Tokens don't exist or are invalid - Run: `python3 scripts/spotify-auth.py` (or with `--headless` if no browser) ### "HTTP Error 400: Bad Request" during token refresh - Credentials changed or are invalid - Re-run setup: `bash skills/spotify-history/scripts/setup.sh` - Or update `credentials/spotify.json` with correct Client ID/Secret ### "HTTP Error 401: Unauthorized" - Token expired and auto-refresh failed - Delete token and re-authenticate: ```bash rm ~/.config/spotify-clawd/token.json python3 scripts/spotify-auth.py ``` ### Headless / No Browser - Use `--headless` flag: `python3 scripts/spotify-auth.py --headless` - Manually open the auth URL on any device - Copy the callback URL (starts with `http://127.0.0.1:8888/callback?code=...`) - Paste it back when prompted ## Security Notes - Tokens stored with 0600 permissions (user-only read/write) - Client secret should be kept private - Redirect URI uses `127.0.0.1` (local only) for security ## Required Scopes - `user-read-recently-played` — recent listening history - `user-top-read` — top artists and tracks - `user-read-playback-state` — current playback - `user-read-currently-playing` — currently playing track

path: README.md
content: # Spotify History & Recommendations

Access your Spotify listening history, top artists/tracks, and get personalized recommendations via the Spotify Web API.

## Quick Start

```bash
# Run setup wizard
bash skills/spotify-history/scripts/setup.sh

# Test it
python3 scripts/spotify-api.py recent
python3 scripts/spotify-api.py top-artists
python3 scripts/spotify-api.py recommend
```

## What It Does

- **Listening History**: See what you've been playing recently
- **Top Artists/Tracks**: Your most-played artists and tracks (4 weeks, 6 months, or all time)
- **Recommendations**: Get personalized music recommendations based on your taste
- **Auto-Refresh**: Tokens refresh automatically - set up once, works forever

## Requirements

- Python 3.6+
- Spotify account (free or premium)
- One-time setup: Spotify Developer App (free, takes 2 minutes)

## Agent Integration

When you ask your agent about music:
- "What have I been listening to?"
- "Who are my top artists?"
- "Recommend new music"

The agent will:
1. Fetch your Spotify data
2. Analyze your taste
3. Combine API data with music knowledge for personalized recommendations

## Documentation

See [SKILL.md](./SKILL.md) for full documentation, troubleshooting, and advanced usage.

## License

MIT


path: scripts/setup.sh
content: #!/usr/bin/env bash
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
WORKSPACE_DIR="$(dirname "$(dirname "$SKILL_DIR")")"
CREDS_DIR="$WORKSPACE_DIR/credentials"
CREDS_FILE="$CREDS_DIR/spotify.json"

echo "🎵 Spotify History Skill Setup"
echo "================================"
echo

# Check if already set up
if [ -f "$CREDS_FILE" ]; then
    echo "✓ Credentials already exist at: $CREDS_FILE"
    read -p "Overwrite? (y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Keeping existing credentials."
        exit 0
    fi
fi

echo "Step 1: Create Spotify Developer App"
echo "-------------------------------------"
echo "1. Go to: https://developer.spotify.com/dashboard"
echo "2. Click 'Create App'"
echo "3. Fill in:"
echo "   - App name: Clawd (or any name)"
echo "   - App description: Personal assistant integration"
echo "   - Redirect URI: http://127.0.0.1:8888/callback"
echo "4. Save and copy your Client ID and Client Secret"
echo
read -p "Press Enter when ready..."
echo

# Get credentials
echo "Step 2: Enter Credentials"
echo "-------------------------"
read -p "Client ID: " CLIENT_ID
read -p "Client Secret: " CLIENT_SECRET
echo

# Validate input
if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ]; then
    echo "❌ Error: Client ID and Secret are required"
    exit 1
fi

# Create credentials directory
mkdir -p "$CREDS_DIR"

# Save credentials
cat > "$CREDS_FILE" <<EOF
{
  "client_id": "$CLIENT_ID",
  "client_secret": "$CLIENT_SECRET",
  "redirect_uri": "http://127.0.0.1:8888/callback"
}
EOF

chmod 600 "$CREDS_FILE"
echo "✓ Credentials saved to: $CREDS_FILE"
echo

# Run auth flow
echo "Step 3: Authorize Spotify Access"
echo "---------------------------------"
echo "Running OAuth flow..."
echo

cd "$WORKSPACE_DIR"
python3 scripts/spotify-auth.py

echo
echo "✅ Setup Complete!"
echo
echo "Test it:"
echo "  python3 scripts/spotify-api.py recent"
echo "  python3 scripts/spotify-api.py top-artists"
echo "  python3 scripts/spotify-api.py recommend"
echo


path: scripts/spotify-api.py
content: #!/usr/bin/env python3
"""
Spotify API helper - fetch listening history and recommendations
"""

import json
import urllib.request
import urllib.parse
import base64
import sys
import os
from pathlib import Path
from datetime import datetime

CLIENT_ID = os.environ.get("SPOTIFY_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("SPOTIFY_CLIENT_SECRET", "")
TOKEN_FILE = Path.home() / ".config" / "spotify-clawd" / "token.json"

def load_token():
    if not TOKEN_FILE.exists():
        print("Error: Not authenticated. Run spotify-auth.py first.")
        sys.exit(1)
    return json.loads(TOKEN_FILE.read_text())

def save_token(token_data):
    TOKEN_FILE.write_text(json.dumps(token_data, indent=2))

def refresh_token(token_data):
    auth_header = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
    
    data = urllib.parse.urlencode({
        "grant_type": "refresh_token",
        "refresh_token": token_data["refresh_token"],
    }).encode()
    
    req = urllib.request.Request(
        "https://accounts.spotify.com/api/token",
        data=data,
        headers={
            "Authorization": f"Basic {auth_header}",
            "Content-Type": "application/x-www-form-urlencoded",
        }
    )
    
    with urllib.request.urlopen(req) as resp:
        new_data = json.load(resp)
    
    # Keep refresh token if not returned
    if "refresh_token" not in new_data:
        new_data["refresh_token"] = token_data["refresh_token"]
    
    save_token(new_data)
    return new_data

def api_request(endpoint, token_data, params=None):
    url = f"https://api.spotify.com/v1{endpoint}"
    if params:
        url += "?" + urllib.parse.urlencode(params)
    
    req = urllib.request.Request(
        url,
        headers={"Authorization": f"Bearer {token_data['access_token']}"}
    )
    
    try:
        with urllib.request.urlopen(req) as resp:
            return json.load(resp)
    except urllib.error.HTTPError as e:
        if e.code == 401:
            # Token expired, refresh and retry
            token_data = refresh_token(token_data)
            req = urllib.request.Request(
                url,
                headers={"Authorization": f"Bearer {token_data['access_token']}"}
            )
            with urllib.request.urlopen(req) as resp:
                return json.load(resp)
        raise

def get_recently_played(token_data, limit=20):
    return api_request("/me/player/recently-played", token_data, {"limit": limit})

def get_top_artists(token_data, time_range="medium_term", limit=20):
    return api_request("/me/top/artists", token_data, {"time_range": time_range, "limit": limit})

def get_top_tracks(token_data, time_range="medium_term", limit=20):
    return api_request("/me/top/tracks", token_data, {"time_range": time_range, "limit": limit})

def get_recommendations(token_data, seed_artists=None, seed_tracks=None, seed_genres=None, limit=20):
    params = {"limit": limit}
    if seed_artists:
        params["seed_artists"] = ",".join(seed_artists[:5])
    if seed_tracks:
        params["seed_tracks"] = ",".join(seed_tracks[:5])
    if seed_genres:
        params["seed_genres"] = ",".join(seed_genres[:5])
    return api_request("/recommendations", token_data, params)

def main():
    if len(sys.argv) < 2:
        print("Usage: spotify-api.py <command>")
        print("Commands: recent, top-artists, top-tracks, recommend")
        return 1
    
    cmd = sys.argv[1]
    token_data = load_token()
    
    if cmd == "recent":
        data = get_recently_played(token_data)
        print("Recently Played:")
        for item in data["items"]:
            track = item["track"]
            played_at = item["played_at"]
            artists = ", ".join(a["name"] for a in track["artists"])
            print(f"  {track['name']} - {artists} ({played_at[:10]})")
    
    elif cmd == "top-artists":
        time_range = sys.argv[2] if len(sys.argv) > 2 else "medium_term"
        data = get_top_artists(token_data, time_range)
        print(f"Top Artists ({time_range}):")
        for i, artist in enumerate(data["items"], 1):
            genres = ", ".join(artist["genres"][:3]) if artist["genres"] else "N/A"
            print(f"  {i}. {artist['name']} [{genres}]")
    
    elif cmd == "top-tracks":
        time_range = sys.argv[2] if len(sys.argv) > 2 else "medium_term"
        data = get_top_tracks(token_data, time_range)
        print(f"Top Tracks ({time_range}):")
        for i, track in enumerate(data["items"], 1):
            artists = ", ".join(a["name"] for a in track["artists"])
            print(f"  {i}. {track['name']} - {artists}")
    
    elif cmd == "recommend":
        # Get top artists for seeds
        top = get_top_artists(token_data, limit=5)
        seed_artists = [a["id"] for a in top["items"][:3]]
        
        data = get_recommendations(token_data, seed_artists=seed_artists)
        print("Recommendations based on your top artists:")
        for track in data["tracks"]:
            artists = ", ".join(a["name"] for a in track["artists"])
            print(f"  {track['name']} - {artists}")
    
    elif cmd == "json":
        # Raw JSON output for any endpoint
        endpoint = sys.argv[2] if len(sys.argv) > 2 else "/me"
        data = api_request(endpoint, token_data)
        print(json.dumps(data, indent=2))
    
    else:
        print(f"Unknown command: {cmd}")
        return 1
    
    return 0

if __name__ == "__main__":
    exit(main())


path: scripts/spotify-auth.py
content: #!/usr/bin/env python3
"""
Spotify OAuth flow - gets access and refresh tokens
"""

import http.server
import urllib.parse
import webbrowser
import json
import base64
import os
from pathlib import Path

# Config
CLIENT_ID = os.environ.get("SPOTIFY_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("SPOTIFY_CLIENT_SECRET", "")
REDIRECT_URI = "http://127.0.0.1:8888/callback"
SCOPES = "user-read-recently-played user-top-read user-read-playback-state user-read-currently-playing"

TOKEN_FILE = Path.home() / ".config" / "spotify-clawd" / "token.json"

auth_code = None

class CallbackHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        global auth_code
        query = urllib.parse.urlparse(self.path).query
        params = urllib.parse.parse_qs(query)
        
        if "code" in params:
            auth_code = params["code"][0]
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(b"<html><body><h1>Success!</h1><p>You can close this window.</p></body></html>")
        elif "error" in params:
            self.send_response(400)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(f"<html><body><h1>Error</h1><p>{params.get('error', ['Unknown'])}</p></body></html>".encode())
        else:
            self.send_response(404)
            self.end_headers()
    
    def log_message(self, format, *args):
        pass  # Suppress logging

def get_auth_url():
    params = {
        "client_id": CLIENT_ID,
        "response_type": "code",
        "redirect_uri": REDIRECT_URI,
        "scope": SCOPES,
    }
    return "https://accounts.spotify.com/authorize?" + urllib.parse.urlencode(params)

def exchange_code(code):
    import urllib.request
    
    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()
    
    req = urllib.request.Request(
        "https://accounts.spotify.com/api/token",
        data=data,
        headers={
            "Authorization": f"Basic {auth_header}",
            "Content-Type": "application/x-www-form-urlencoded",
        }
    )
    
    with urllib.request.urlopen(req) as resp:
        return json.load(resp)

def save_token(token_data):
    TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
    TOKEN_FILE.write_text(json.dumps(token_data, indent=2))
    TOKEN_FILE.chmod(0o600)
    print(f"Token saved to {TOKEN_FILE}")

def main():
    if not CLIENT_ID or not CLIENT_SECRET:
        print("Error: Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET environment variables")
        return 1
    
    # Start callback server
    server = http.server.HTTPServer(("127.0.0.1", 8888), CallbackHandler)
    
    # Open browser
    auth_url = get_auth_url()
    print(f"Opening browser for authorization...")
    print(f"If it doesn't open, visit: {auth_url}")
    webbrowser.open(auth_url)
    
    # Wait for callback
    print("Waiting for authorization...")
    while auth_code is None:
        server.handle_request()
    
    # Exchange code for token
    print("Exchanging code for token...")
    token_data = exchange_code(auth_code)
    
    save_token(token_data)
    print("Done! Spotify is now connected.")
    
    return 0

if __name__ == "__main__":
    exit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)