
Spotify Api
- 107 installs
- 15 repo stars
- Updated October 30, 2025
- fabioc-aloha/spotify-skill
Call Spotify Web API endpoints to search catalogs, manage playlists, read playback state, and embed music features into apps, agents, or internal tools with correct OAuth scopes.
About
Spotify API skill from fabioc-aloha/spotify-skill for developers integrating music services. Documents authentication, common endpoints, pagination, and practical patterns for building search, playlist, and playback experiences against Spotify's public API during application development.
- OAuth and scope guidance for Spotify Web API
- Search tracks, albums, artists, and playlists
- Read and modify user library and playlist data
- Playback and metadata patterns for app features
Spotify Api by the numbers
- 107 all-time installs (skills.sh)
- Ranked #2,959 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 23, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fabioc-aloha/spotify-skill --skill spotify-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 107 |
|---|---|
| repo stars | ★ 15 |
| Last updated | October 30, 2025 |
| Repository | fabioc-aloha/spotify-skill ↗ |
What it does
Call Spotify Web API endpoints to search catalogs, manage playlists, read playback state, and embed music features into apps, agents, or internal tools with correct OAuth scopes.
Files
Spotify API Skill
Version: 0.9.1 | Release Date: October 22, 2025
Overview
This skill directly interacts with the Spotify Web API to manage music and playlists.
⚡ Unique Capability: Image Generation
🎨 This skill can GENERATE IMAGES - something Claude cannot do natively! It creates custom SVG-based cover art for Spotify playlists with large, readable typography optimized for thumbnail viewing. Each cover art is dynamically generated with theme-appropriate colors, gradients, and text layouts.
Use this skill when you need to:
- 🎨 Generate cover art images - Create custom playlist covers (Claude's built-in image generation limitation is bypassed!)
- 🎵 Create playlists from artist names, themes, or specific songs
- 🔍 Search for tracks, artists, albums
- ➕ Add/remove tracks from playlists
- ▶️ Control playback (play, pause, skip)
- 📊 Get user data (profile, top tracks, listening history)
When to use this skill: The user wants you to create a playlist, search for music, manage their Spotify account, or generate custom cover art images.
Core Capabilities
1. 🎨 Cover Art Image Generation - Generate custom images with SVG → PNG conversion (Claude cannot generate images natively!) 2. Intelligent Playlist Creation - Create playlists by artist, theme, lyrics, or song list 3. Playlist Management - Create, list, update, delete playlists 4. Search & Discovery - Find tracks, artists, albums, playlists 5. Track Management - Add/remove tracks, get recommendations 6. Playback Control - Play, pause, skip, control volume 7. User Library - Access saved tracks, profile, listening history
Quick Start
All Spotify API operations use the SpotifyClient class from scripts/spotify_client.py. The client handles OAuth authentication and provides methods for all operations.
Prerequisites
1. Enable Network Access (REQUIRED)
⚠️ This skill requires network access to reach api.spotify.com
In Claude Desktop, you must enable network egress:
- Go to Settings → Developer → Allow network egress
- Toggle it ON (blue)
- Under "Domain allowlist", choose either:
- "All domains" (easiest), OR
- "Specified domains" and add
api.spotify.com(more secure/restricted) - This allows the skill to make API calls to Spotify's servers
Without network access enabled, API calls will fail with connection errors.
2. Install Dependencies
pip install -r requirements.txtRequired packages:
requests>=2.31.0- HTTP requests for Spotify Web APIpython-dotenv>=1.0.0- Environment variable managementcairosvg>=2.7.0- SVG to PNG conversion for image generationpillow>=10.0.0- Image processing for cover art creation
💡 Note: Thecairosvgandpillowpackages enable image generation - allowing this skill to create cover art images even though Claude cannot generate images natively!
Basic Setup
The easiest way to initialize the client is using credentials from environment variables (loaded from .env file):
from spotify_client import create_client_from_env
# Initialize client from environment variables (.env file)
client = create_client_from_env()
# If you have a refresh token, refresh the access token
if client.refresh_token:
client.refresh_access_token()Alternatively, you can manually provide credentials:
from spotify_client import SpotifyClient
# Initialize with credentials directly
client = SpotifyClient(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
redirect_uri="http://localhost:8888/callback",
refresh_token="YOUR_REFRESH_TOKEN" # if available
)
# Refresh to get current access token
if client.refresh_token:
client.refresh_access_token()Common Operations
List ALL user playlists (with pagination):
# Get all playlists - handles pagination automatically
all_playlists = []
offset = 0
limit = 50 # Max allowed per request
while True:
playlists = client.get_user_playlists(limit=limit, offset=offset)
if not playlists:
break # No more playlists
all_playlists.extend(playlists)
offset += limit
if len(playlists) < limit:
break # Last page (fewer than limit returned)
print(f"Total playlists: {len(all_playlists)}")
for playlist in all_playlists:
print(f"- {playlist['name']} ({playlist['tracks']['total']} tracks)")Create a new playlist:
playlist = client.create_playlist(
name="My Awesome Playlist",
description="A curated collection",
public=True
)Search for tracks:
results = client.search_tracks(query="artist:The Beatles", limit=20)Add tracks to playlist:
client.add_tracks_to_playlist(
playlist_id="playlist_123",
track_ids=["track_1", "track_2", "track_3"]
)Playlist Management Workflows
List All User Playlists
Important: Users may have more than 50 playlists. Always use pagination to get ALL playlists:
# Get ALL playlists using pagination
all_playlists = []
offset = 0
limit = 50 # Spotify's max per request
while True:
batch = client.get_user_playlists(limit=limit, offset=offset)
if not batch:
break # No more playlists to fetch
all_playlists.extend(batch)
print(f"Fetched {len(batch)} playlists (total so far: {len(all_playlists)})")
offset += limit
if len(batch) < limit:
break # Last page - fewer results than limit means we're done
print(f"\n✓ Total playlists found: {len(all_playlists)}")
# Display all playlists with details
for i, playlist in enumerate(all_playlists, 1):
print(f"{i}. {playlist['name']}")
print(f" Tracks: {playlist['tracks']['total']}")
print(f" Public: {playlist['public']}")
print(f" ID: {playlist['id']}")Playlist Creation Workflows
By Artist/Band Name
Create a playlist containing all or most popular tracks by a specific artist:
# STEP 1: Search for the artist by name
artists = client.search_artists(query="The Beatles", limit=1)
if not artists:
print("Artist not found")
# Handle error: artist doesn't exist or name is misspelled
else:
artist_id = artists[0]['id'] # Get Spotify ID of first result
# STEP 2: Get the artist's most popular tracks
# Note: Spotify API returns up to 10 top tracks per artist
tracks = client.get_artist_top_tracks(artist_id=artist_id)
track_ids = [t['id'] for t in tracks] # Extract just the track IDs
# STEP 3: Create a new playlist and add the tracks
playlist = client.create_playlist(name="The Beatles Collection")
client.add_tracks_to_playlist(playlist['id'], track_ids)
print(f"Created playlist with {len(track_ids)} tracks")By Theme/Mood
Create thematic playlists by searching for tracks matching mood keywords:
# STEP 1: Define search queries for your theme
# Spotify search syntax: "genre:indie mood:chill" or "genre:indie year:2020-2024"
theme_queries = [
"genre:indie mood:chill", # Search for chill indie tracks
"genre:indie year:2020-2024" # Search for recent indie tracks
]
# STEP 2: Search for tracks matching each query
all_tracks = []
for query in theme_queries:
results = client.search_tracks(query=query, limit=50) # Get up to 50 per query
all_tracks.extend(results) # Combine results from all queries
# STEP 3: Remove duplicates (same track may match multiple queries)
# Use set() with track IDs to keep only unique tracks
unique_track_ids = list(set(t['id'] for t in all_tracks))
# STEP 4: Create playlist with unique tracks (limit to 100 for reasonable size)
playlist = client.create_playlist(name="Chill Indie Evening")
client.add_tracks_to_playlist(playlist['id'], unique_track_ids[:100])
print(f"Created playlist with {len(unique_track_ids[:100])} tracks")By Lyrics Content
Search for tracks with specific lyrical themes using Spotify's search:
# STEP 1: Define keywords related to lyrical content
# Note: Spotify search indexes track/artist names and some metadata,
# not full lyrics, so results are based on title/description matching
queries = ["love", "heartbreak", "summer", "midnight"]
# STEP 2: Search for tracks matching each keyword
all_tracks = []
for keyword in queries:
results = client.search_tracks(query=keyword, limit=20) # 20 tracks per keyword
all_tracks.extend(results)
# STEP 3: Remove duplicates (same track may match multiple keywords)
# Use set() with track IDs to keep only unique tracks
unique_track_ids = list(set(t['id'] for t in all_tracks))
print(f"Found {len(all_tracks)} total matches, {len(unique_track_ids)} unique tracks")
# STEP 4: Create playlist (limit to 100 tracks for reasonable size)
playlist = client.create_playlist(name="Love & Heartbreak")
client.add_tracks_to_playlist(playlist['id'], unique_track_ids[:100])
print(f"Created playlist with {len(unique_track_ids[:100])} unique tracks")From Specific Song List
Create a playlist from a user-provided list of track URIs or search terms:
# STEP 1: Get the list of songs from the user
# User provides song names (can also use Spotify URIs like "spotify:track:...")
song_list = ["Shape of You", "Blinding Lights", "As It Was"]
# STEP 2: Search for each song and collect track IDs
track_ids = []
for song_name in song_list:
results = client.search_tracks(query=song_name, limit=1) # Get best match
if results:
track_ids.append(results[0]['id']) # Add first result's ID
print(f"✓ Found: {results[0]['name']} by {results[0]['artists'][0]['name']}")
else:
print(f"✗ Not found: {song_name}") # Song doesn't exist or name is wrong
# STEP 3: Create playlist with found tracks
playlist = client.create_playlist(name="My Favorites")
if track_ids:
client.add_tracks_to_playlist(playlist['id'], track_ids)
print(f"Created playlist with {len(track_ids)}/{len(song_list)} tracks")
else:
print("No tracks found - playlist is empty")🎨 Cover Art Image Generation
⚡ UNIQUE CAPABILITY: This skill can generate images!
>
Claude cannot generate images natively, but this skill bypasses that limitation by creating custom SVG graphics and converting them to PNG images for Spotify playlist covers.
🚨 MANDATORY: USE THE COVER ART LLM GUIDE
>
⚠️ DO NOT attempt to generate cover art without first reading the complete execution guide.
>
➡️ READ THIS FILE FIRST: [references/COVER_ART_LLM_GUIDE.md](references/COVER_ART_LLM_GUIDE.md)
>
This guide is REQUIRED and contains:
- Complete step-by-step execution instructions
- How to analyze playlist content to determine appropriate colors
- Genre-to-color and mood-to-color mapping tables
- Typography rules and accessibility requirements
- Edge case handling and quality checklist
>
Do not proceed with cover art generation without consulting this guide first.
⚠️ Required Scope for Upload
To upload cover art to Spotify, you MUST have the `ugc-image-upload` scope enabled:
1. Go to Spotify Developer Dashboard 2. Select your app 3. Ensure ugc-image-upload scope is included in your authorization 4. Re-run OAuth flow to get a new refresh token with this scope: python get_refresh_token.py 5. Update your .env file with the new refresh token
Without this scope: You'll get a 401 error when trying to upload. However, you can still generate cover art images locally and upload them manually via Spotify's web/mobile app.
📖 Having trouble? See COVER_ART_TROUBLESHOOTING.md for detailed solutions.
Basic Usage Example
⚠️ Remember: Read [references/COVER_ART_LLM_GUIDE.md](references/COVER_ART_LLM_GUIDE.md) before generating cover art!
from cover_art_generator import CoverArtGenerator
# Initialize generator (uses same credentials as SpotifyClient)
art_gen = CoverArtGenerator(client_id, client_secret, access_token)
# Generate and upload cover art
# (Follow the LLM guide for how to determine appropriate colors)
art_gen.create_and_upload_cover(
playlist_id=playlist['id'],
title="Beast Mode", # Main title (large text)
subtitle="Gym", # Optional subtitle
gradient_start="#E63946", # Colors determined from guide
gradient_end="#1D1D1D",
text_color="#FFFFFF"
)All cover art generation workflows, color selection guidance, and advanced features are documented in [references/COVER_ART_LLM_GUIDE.md](references/COVER_ART_LLM_GUIDE.md).
Advanced Operations
Get Recommendations
# Get AI-powered music recommendations based on seed artists/tracks/genres
recommendations = client.get_recommendations(
seed_artists=["artist_id_1"], # Spotify IDs of artists
seed_tracks=["track_id_1"], # Spotify IDs of tracks
limit=50 # Number of recommendations to get
)
# Returns: List of recommended tracks similar to the seedsAccess User Profile
# Get information about the current authenticated user
profile = client.get_current_user()
# Returns: user_id, display_name, email, followers, images, country, etc.
print(f"Logged in as: {profile['display_name']}")
print(f"User ID: {profile['id']}")Get User's Top Items
# Get user's most played artists over different time periods
top_artists = client.get_top_items(
item_type="artists",
limit=20,
time_range="medium_term" # Options: short_term (~4 weeks), medium_term (~6 months), long_term (~years)
)
print(f"Top artist: {top_artists[0]['name']}")
# Get user's most played tracks
top_tracks = client.get_top_items(
item_type="tracks",
limit=20,
time_range="short_term" # Recent listening (last ~4 weeks)
)
print(f"Most played track: {top_tracks[0]['name']} by {top_tracks[0]['artists'][0]['name']}")Playback Control
# STEP 1: Start playback of a playlist or album
client.start_playback(
device_id="device_123", # Optional: specific device
context_uri="spotify:playlist:playlist_id", # What to play (playlist/album/artist URI)
offset=0 # Optional: start at track 0
)
# STEP 2: Pause playback
client.pause_playback(device_id="device_123")
# STEP 3: Skip to next track
client.next_track(device_id="device_123")
# STEP 4: Check what's currently playing
current = client.get_currently_playing()
if current and current.get('item'):
track = current['item']
print(f"Now playing: {track['name']} by {track['artists'][0]['name']}")
else:
print("Nothing is currently playing")Authentication & Credentials
See references/authentication_guide.md for detailed OAuth flow setup and credential management. Ensure credentials are available in environment variables or passed at initialization:
SPOTIFY_CLIENT_IDSPOTIFY_CLIENT_SECRETSPOTIFY_REDIRECT_URISPOTIFY_ACCESS_TOKEN(optional, can use refresh token)SPOTIFY_REFRESH_TOKEN(for token refresh)
API Reference
See references/api_reference.md for complete Spotify API endpoint documentation, rate limits, response formats, and error handling patterns.
Scripts
spotify_client.py
Comprehensive Python wrapper for all Spotify Web API operations. Handles authentication, token management, rate limiting, and provides methods for:
- Authentication and token refresh
- Playlist CRUD operations
- Search (tracks, artists, albums, playlists)
- Track/URI management
- User data access
- Playback control
- Recommendations engine
playlist_creator.py
High-level utility for intelligent playlist creation from various sources (artist, theme, lyrics, song list). Encapsulates common workflows and handles track deduplication and limit management.
---
For Developers Building Apps (Advanced)
Note: The features below are for developers building web applications, NOT for direct playlist creation tasks.
If the user is asking you to build an application or export data, see:
ADVANCED_USAGE.md- Application development patternsscripts/export_data.py- Export Spotify data as JSONSpotifyAPIWrapperclass - Error handling for production apps
For playlist creation and music management, use the workflows above.
# Spotify API Credentials
# Get these from: https://developer.spotify.com/dashboard
# Required: Your Spotify App Client ID
SPOTIFY_CLIENT_ID=your_client_id_here
# Required: Your Spotify App Client Secret
SPOTIFY_CLIENT_SECRET=your_client_secret_here
# Required: OAuth Redirect URI (must match your app settings)
SPOTIFY_REDIRECT_URI=http://127.0.0.1:8888/callback
# Required: Refresh Token (generate using get_refresh_token.py)
SPOTIFY_REFRESH_TOKEN=your_refresh_token_here
# Optional: Access Token (will be auto-refreshed using refresh token)
# SPOTIFY_ACCESS_TOKEN=your_access_token_here
Spotify API Skill - Advanced Usage
Overview
This document covers advanced features for robust application development, including error handling, fallback data, validation, and data export.
Features for Application Development
1. Credential Validation
Validate credentials before making API calls:
from spotify_client import validate_credentials, get_validation_errors
# Check credential status
validation = validate_credentials()
if not validation['all_valid']:
errors = get_validation_errors()
for error in errors:
print(error)
# Handle missing credentialsReturns:
{
'client_id': True/False,
'client_secret': True/False,
'refresh_token': True/False,
'redirect_uri': True/False,
'all_valid': True/False # All required credentials present
}2. SpotifyAPIWrapper - Graceful Error Handling
The SpotifyAPIWrapper provides robust error handling with optional fallback data:
from spotify_client import SpotifyAPIWrapper
# Create wrapper with fallback enabled (default)
wrapper = SpotifyAPIWrapper(use_fallback=True)
# Check if API is available
if wrapper.is_available():
print("API connected!")
else:
print(f"Using fallback data: {wrapper.get_error()}")
# These calls return empty lists/mock data if API fails
user = wrapper.get_user_profile()
playlists = wrapper.get_user_playlists(limit=20)
tracks = wrapper.search_tracks("Beatles", limit=10)Available Methods:
get_user_profile()- Returns user data or mock profileget_user_playlists(limit)- Returns playlists or empty listsearch_tracks(query, limit)- Returns tracks or empty listcreate_playlist(name, description, public)- Returns playlist or Noneis_available()- Check if API is initializedget_error()- Get initialization error if any
Use Cases:
- React/Web Apps: Always have data to render, even without API access
- Development: Test UI without valid credentials
- Production: Graceful degradation when API is unavailable
3. Data Export for Static Apps
Export Spotify data as JSON files for apps that don't need real-time API calls:
python spotify-api/scripts/export_data.pyExports:
user_profile.json- User profile dataplaylists.json- All user playliststop_artists_medium_term.json- Top artiststop_tracks_medium_term.json- Top tracks
Use in React:
import userData from './exported_data/user_profile.json';
import playlists from './exported_data/playlists.json';
function MyComponent() {
return (
<div>
<h1>Welcome, {userData.display_name}!</h1>
<PlaylistList playlists={playlists} />
</div>
);
}4. Network Detection
Automatic detection of network restrictions with helpful error messages:
from spotify_client import check_network_access, NetworkAccessError
try:
check_network_access()
print("Network OK")
except NetworkAccessError as e:
print(e) # Displays setup instructionsWhen network is blocked, users see:
❌ NETWORK ACCESS BLOCKED
The Spotify API skill cannot access api.spotify.com.
🔧 FIX: Enable network egress in Claude Desktop:
1. Open Claude Desktop → Settings → Developer
2. Toggle 'Allow network egress' to ON (blue)
3. Set 'Domain allowlist' to either:
• 'All domains' (easiest), OR
• 'Specified domains' and add 'api.spotify.com'
📖 See GETTING_STARTED.md for detailed instructions.Complete Example: React App Integration
"""
Prepare data for a React application with fallback support.
"""
from spotify_client import SpotifyAPIWrapper
import json
def prepare_react_data():
# Use wrapper with fallback
wrapper = SpotifyAPIWrapper(use_fallback=True)
# Build data structure
app_data = {
'user': wrapper.get_user_profile(),
'playlists': wrapper.get_user_playlists(limit=20),
'recentTracks': wrapper.search_tracks("recent", limit=10),
'isLiveData': wrapper.is_available(),
'dataSource': 'spotify-api' if wrapper.is_available() else 'fallback'
}
# Save as JSON for React app
with open('public/spotify-data.json', 'w') as f:
json.dump(app_data, f, indent=2)
print(f"✅ Data prepared ({app_data['dataSource']})")
return app_data
if __name__ == '__main__':
prepare_react_data()In your React app:
import React, { useEffect, useState } from 'react';
import spotifyData from './spotify-data.json';
function SpotifyDashboard() {
const [data, setData] = useState(spotifyData);
useEffect(() => {
if (data.isLiveData) {
console.log('Using live Spotify data');
} else {
console.warn('Using fallback data:', data.dataSource);
}
}, [data]);
return (
<div>
<h1>Welcome, {data.user.display_name}!</h1>
{!data.isLiveData && (
<div className="warning">
Using sample data. Configure Spotify API for live data.
</div>
)}
<PlaylistGrid playlists={data.playlists} />
</div>
);
}Error Handling Best Practices
1. Always Validate First
from spotify_client import validate_credentials, get_validation_errors
validation = validate_credentials()
if not validation['all_valid']:
errors = get_validation_errors()
# Show errors to user
return2. Use Wrapper for User-Facing Apps
# ✅ Good: Graceful degradation
wrapper = SpotifyAPIWrapper(use_fallback=True)
playlists = wrapper.get_user_playlists() # Returns [] on error
# ❌ Avoid: Unhandled errors in production
client = create_client_from_env()
playlists = client.get_user_playlists() # May raise exception3. Check Network Before Critical Operations
from spotify_client import check_network_access, NetworkAccessError
try:
check_network_access()
# Proceed with API calls
except NetworkAccessError as e:
print(e)
# Guide user to enable network access4. Export Data for Static Deployments
# Generate JSON files once
python spotify-api/scripts/export_data.py
# Deploy static site with pre-generated data
# No runtime API calls neededTesting Your Integration
Test with Valid Credentials
python spotify-api/scripts/test_credentials.pyTest Wrapper Functionality
python example_wrapper_usage.pyTest Data Export
python spotify-api/scripts/export_data.py
ls exported_data/Troubleshooting
"Missing credentials" Error
# Check what's missing
from spotify_client import get_validation_errors
errors = get_validation_errors()
for error in errors:
print(error)Fix: 1. Copy spotify-api/.env.example to spotify-api/.env 2. Add your credentials from Spotify Developer Dashboard 3. Run python get_refresh_token.py to get refresh token
"Network access blocked" Error
Fix: 1. Open Claude Desktop → Settings → Developer 2. Enable "Allow network egress" 3. Add api.spotify.com to domain allowlist (or allow all domains) 4. Restart Claude Desktop
Wrapper Returns Empty Data
wrapper = SpotifyAPIWrapper(use_fallback=True)
if not wrapper.is_available():
error = wrapper.get_error()
print(f"API unavailable: {error}")
# Using fallback dataCommon causes:
- Missing credentials
- Network access blocked
- Invalid refresh token (re-run
get_refresh_token.py)
See Also
- [GETTING_STARTED.md](../GETTING_STARTED.md) - Initial setup
- [USER_GUIDE.md](../USER_GUIDE.md) - Complete API reference
- [example_wrapper_usage.py](../example_wrapper_usage.py) - Wrapper examples
- [export_data.py](scripts/export_data.py) - Data export script
Cover Art Upload Troubleshooting
401 Unauthorized Error
If you get a 401 Unauthorized error when trying to upload cover art to Spotify, it means your access token doesn't have the required ugc-image-upload scope.
The Issue
Spotify requires the ugc-image-upload scope to upload custom images to playlists. This is a security measure to ensure only authorized apps can modify playlist cover art.
Error message:
✗ Failed to upload cover art: 401 Unauthorized
⚠️ MISSING SCOPE: The 'ugc-image-upload' scope is required!Solutions
Option 1: Re-authorize with Correct Scope (Recommended)
This enables automatic cover art upload directly from the skill.
Steps:
1. Open your Spotify Developer Dashboard
- Go to https://developer.spotify.com/dashboard
- Log in with your Spotify account
- Select your app
2. Verify your app settings
- Make sure your redirect URI is set (e.g.,
http://127.0.0.1:8888/callback) - Note: The scope is requested during authorization, not configured in the dashboard
3. Re-run the OAuth flow to get a new refresh token
python get_refresh_token.pyThis script now includes ugc-image-upload in the requested scopes:
- Playlist management (create, modify, read)
- User library access
- Playback control
- User profile and top items
- 🎨 ugc-image-upload (for cover art upload!)
4. Update your `.env` file
- Copy the new refresh token from the output
- Update
SPOTIFY_REFRESH_TOKENin your.envfile:
SPOTIFY_REFRESH_TOKEN=your_new_refresh_token_here5. Test the upload
python spotify-api/test_cover_art.pyOption 2: Generate Locally, Upload Manually
If you prefer not to re-authorize or want to review cover art before uploading:
Steps:
1. Generate cover art locally (doesn't require upload scope):
from cover_art_generator import CoverArtGenerator
generator = CoverArtGenerator(client_id, client_secret, access_token)
# Generate without uploading
png_path = generator.generate_cover_art(
title="My Playlist",
subtitle="2024",
theme="summer",
output_path="my_cover.png"
)
print(f"Cover saved to: {png_path}")2. Upload manually via Spotify:
- Open Spotify (web or desktop app)
- Go to your playlist
- Click the three dots (...) → "Edit details"
- Click "Change image"
- Select the generated PNG file
- Click "Save"
Understanding Spotify Scopes
Required Scopes for Full Functionality
SCOPES = [
# Playback control
'user-read-playback-state',
'user-modify-playback-state',
'user-read-currently-playing',
# Playlist management
'playlist-read-private',
'playlist-read-collaborative',
'playlist-modify-public',
'playlist-modify-private',
# Library access
'user-library-read',
'user-library-modify',
# User data
'user-top-read',
'user-read-private',
'user-read-email',
# Cover art upload (NEW!)
'ugc-image-upload', # Required for cover art upload
]Checking Your Current Scopes
Unfortunately, Spotify doesn't provide an API endpoint to check which scopes a token has. If you're unsure:
1. Check when you last authorized (before or after ugc-image-upload was added to get_refresh_token.py) 2. Try to upload cover art - if it fails with 401, you need to re-authorize 3. Re-run get_refresh_token.py to be safe
Verification
After re-authorizing with the correct scope, verify it works:
# Test cover art generation and upload
cd spotify-api
python test_cover_art.py
# Or test in your code
python -c "
from scripts.cover_art_generator import CoverArtGenerator
import os
gen = CoverArtGenerator(
os.getenv('SPOTIFY_CLIENT_ID'),
os.getenv('SPOTIFY_CLIENT_SECRET'),
os.getenv('SPOTIFY_ACCESS_TOKEN')
)
# Generate test image
gen.generate_cover_art(
title='Test',
subtitle='Scope Check',
theme='energetic',
output_path='test.png'
)
print('✓ Image generated successfully!')
# Try to upload (requires playlist ID)
# gen.upload_cover_image('YOUR_PLAYLIST_ID', 'test.png')
"Common Issues
"I re-authorized but still get 401"
1. Make sure you copied the new refresh token to .env 2. Restart any running Python processes 3. Check that get_refresh_token.py includes ugc-image-upload in the SCOPES list 4. Delete any cached tokens and re-authorize
"The authorization page doesn't show ugc-image-upload"
The authorization page shows human-readable descriptions, not the exact scope names. Look for language like:
- "Upload images"
- "Modify playlist images"
- "Change playlist cover art"
"I don't want to give this permission"
That's fine! You can still use all the cover art generation features. Just use Option 2 (generate locally, upload manually) instead of automatic upload.
Feature Availability
| Feature | Requires ugc-image-upload? |
|---|---|
| Generate cover art SVG | ❌ No |
| Convert SVG to PNG | ❌ No |
| Save PNG locally | ❌ No |
| Upload to Spotify API | ✅ Yes |
Need Help?
If you're still having issues:
1. Check the Spotify Web API documentation: https://developer.spotify.com/documentation/web-api 2. Verify your app settings in the dashboard 3. Make sure your redirect URI exactly matches what's in your code 4. Check for any typos in your .env file 5. Try with a brand new Spotify app (fresh client ID/secret)
Summary
The cover art generation skill can create images locally without any special permissions. However, to automatically upload them to Spotify via the API, you need the ugc-image-upload scope. Re-run get_refresh_token.py to get a token with this scope, or generate cover art locally and upload it manually through Spotify's interface.
Spotify Web API Reference
Table of Contents
1. Rate Limiting 2. Authentication 3. Response Format 4. Common Endpoints 5. Error Handling 6. Data Types
Rate Limiting
Spotify API enforces rate limiting to ensure service stability.
- Rate Limit: 429,400 requests per 30-minute period (general limit)
- Header:
Retry-Afterindicates seconds to wait before retrying - Response Code:
429 Too Many Requests
Best Practices:
- Batch requests when possible (e.g., get up to 50 tracks in single request)
- Implement exponential backoff for retries
- Cache responses when appropriate
- Use offset/pagination for large result sets
Authentication
OAuth 2.0 Authorization Code Flow
Spotify uses OAuth 2.0 for user authentication and authorization.
Endpoints:
- Authorization:
https://accounts.spotify.com/authorize - Token:
https://accounts.spotify.com/api/token
Required Scopes:
playlist-modify-public
playlist-modify-private
user-library-read
user-library-modify
user-read-private
user-read-email
user-top-read
user-read-currently-playing
user-modify-playback-state
user-read-playback-stateToken Expiration:
- Access tokens expire in 3600 seconds (1 hour)
- Use refresh token to obtain new access token without user re-authentication
- Refresh tokens do not expire (unless revoked by user)
Response Format
Standard Response Structure
{
"href": "https://api.spotify.com/v1/...",
"items": [...],
"limit": 20,
"next": "https://api.spotify.com/v1/...?offset=20",
"offset": 0,
"previous": null,
"total": 100
}Pagination
- Use
limitparameter to control items per page (max 50) - Use
offsetparameter for pagination nextURL provides next page,previousURL provides previous page
Example:
GET /v1/me/playlists?limit=50&offset=0
GET /v1/me/playlists?limit=50&offset=50Common Endpoints
Playlists
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/me/playlists | Get current user's playlists |
| POST | /v1/users/{user_id}/playlists | Create playlist |
| GET | /v1/playlists/{playlist_id} | Get playlist details |
| PUT | /v1/playlists/{playlist_id} | Update playlist |
| DELETE | /v1/playlists/{playlist_id} | Delete (unfollow) playlist |
| GET | /v1/playlists/{playlist_id}/tracks | Get playlist tracks |
| POST | /v1/playlists/{playlist_id}/tracks | Add tracks to playlist |
| DELETE | /v1/playlists/{playlist_id}/tracks | Remove tracks from playlist |
Notes:
- Maximum 100 tracks per add/remove request
- Track order is preserved
- Creating/modifying requires playlist-modify scope
Search
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/search | Search across types |
Query Parameters:
q: Search querytype:track,artist,album,playlist(comma-separated)limit: 1-50 (default 20)offset: For pagination
Search Query Syntax:
# Basic search
q=The Beatles
# By field
q=artist:The Beatles
q=track:Yesterday
q=album:Abbey Road
q=year:1965
# Combined
q=artist:The Beatles track:Yesterday
q=genre:rock year:1970-1979
# Exclude
q=artist:The Beatles -liveArtists
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/artists/{artist_id} | Get artist details |
| GET | /v1/artists/{artist_id}/top-tracks | Get artist top tracks |
| GET | /v1/artists/{artist_id}/albums | Get artist albums |
| GET | /v1/artists/{artist_id}/related-artists | Get related artists |
Tracks
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/tracks/{track_id} | Get track details |
| GET | /v1/tracks | Get multiple tracks (comma-separated IDs) |
| GET | /v1/audio-features/{track_id} | Get audio features |
User
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/me | Get current user profile |
| GET | /v1/users/{user_id} | Get user profile |
| GET | /v1/me/top/{type} | Get user's top items (tracks/artists) |
| GET | /v1/me/tracks | Get user's saved tracks |
| PUT | /v1/me/tracks | Save tracks |
| DELETE | /v1/me/tracks | Remove saved tracks |
| GET | /v1/me/tracks/contains | Check if tracks are saved |
Time Ranges:
long_term: ~6 monthsmedium_term: ~6 weeks (default)short_term: ~4 weeks
Recommendations
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/recommendations | Get recommendations |
| GET | /v1/recommendations/available-genre-seeds | Get available genres |
Parameters:
seed_artists: Up to 5 artist IDsseed_tracks: Up to 5 track IDsseed_genres: Up to 5 genres- Total seeds cannot exceed 5
limit: 1-100 (default 20)- Audio feature parameters:
min_energy,max_energy,target_popularity, etc.
Playback
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/me/player | Get playback state |
| GET | /v1/me/player/currently-playing | Get currently playing |
| GET | /v1/me/player/devices | Get available devices |
| PUT | /v1/me/player/play | Start/resume playback |
| PUT | /v1/me/player/pause | Pause playback |
| POST | /v1/me/player/next | Skip to next |
| POST | /v1/me/player/previous | Previous track |
| PUT | /v1/me/player/seek | Seek to position |
| PUT | /v1/me/player/repeat | Set repeat mode |
| PUT | /v1/me/player/shuffle | Enable/disable shuffle |
| PUT | /v1/me/player/volume | Set volume |
Playback Context Types:
spotify:playlist:{playlist_id}spotify:album:{album_id}spotify:artist:{artist_id}
Error Handling
HTTP Status Codes
| Code | Meaning | Action |
|---|---|---|
| 200 | OK | Success |
| 201 | Created | Resource created |
| 204 | No Content | Success (no response body) |
| 400 | Bad Request | Invalid parameters |
| 401 | Unauthorized | Invalid/expired token |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource not found |
| 429 | Too Many Requests | Rate limited (see header) |
| 500 | Server Error | Spotify service error |
Error Response Format
{
"error": {
"status": 404,
"message": "The requested resource was not found"
}
}Retry Strategy
1. Always check Retry-After header for 429 responses 2. Implement exponential backoff: wait 1s, 2s, 4s, 8s, etc. 3. Maximum 3-5 retries recommended 4. For 5xx errors, retry with backoff
Data Types
Track Object
{
"id": "11dFghVXANMlKmJXsNCQvb",
"name": "Yellow",
"artists": [{
"id": "frXchxsSQrCQqf5K25zXiA",
"name": "Coldplay"
}],
"album": {
"id": "0VjIjW4GlUZAMYd2vXMwbU",
"name": "Parachutes"
},
"duration_ms": 269373,
"popularity": 85,
"external_ids": {
"isrc": "GBUM71000059",
"ean": "5099749503584",
"upc": "5099749503584"
},
"uri": "spotify:track:11dFghVXANMlKmJXsNCQvb"
}Playlist Object
{
"id": "37i9dQZF1DX",
"name": "New Music Friday",
"description": "Your weekly update of new tracks featured on Spotify's New Music Friday.",
"public": true,
"collaborative": false,
"followers": {
"total": 1234567
},
"owner": {
"id": "spotify",
"display_name": "Spotify"
},
"tracks": {
"total": 50,
"href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX/tracks"
},
"uri": "spotify:playlist:37i9dQZF1DX"
}Artist Object
{
"id": "0TnOYISbd1XYRBk9FJ3x0V",
"name": "Pitbull",
"genres": ["reggaeton", "latin"],
"popularity": 89,
"followers": {
"total": 28150384
},
"images": [{
"url": "https://i.scdn.co/...",
"height": 640,
"width": 640
}],
"uri": "spotify:artist:0TnOYISbd1XYRBk9FJ3x0V"
}User Object
{
"id": "thelinmichael",
"display_name": "Lín",
"email": "user@example.com",
"followers": {
"total": 150
},
"images": [],
"external_urls": {
"spotify": "https://open.spotify.com/user/thelinmichael"
},
"uri": "spotify:user:thelinmichael"
}Audio Features Object
{
"acousticness": 0.00242,
"danceability": 0.585,
"energy": 0.842,
"instrumentalness": 0.00686,
"key": 9,
"liveness": 0.0646,
"loudness": -5.883,
"mode": 0,
"speechiness": 0.0556,
"tempo": 130.039,
"time_signature": 4,
"valence": 0.428
}Audio Features Definitions:
acousticness(0-1): Likelihood of acoustic sounddanceability(0-1): Suitability for dancingenergy(0-1): Intensity and activityinstrumentalness(0-1): Lack of vocalsliveness(0-1): Presence of audienceloudness(dB): Overall loudnessmode(0/1): Minor/Major keyspeechiness(0-1): Presence of spoken wordstempo(BPM): Overall speedvalence(0-1): Musical positiveness/happiness
Spotify API Authentication Guide
Table of Contents
1. Prerequisites 2. Setting Up Your Spotify App 3. OAuth 2.0 Flow 4. Credential Management 5. Token Management 6. Scopes Explained
Prerequisites
Network Access (Claude Desktop Users)
⚠️ IMPORTANT: If using this skill in Claude Desktop, you must enable network access first:
Required Settings: 1. Open Claude Desktop → Settings → Developer 2. Enable "Allow network egress" (toggle to ON/blue) 3. Set "Domain allowlist" to either:
- "All domains" (easiest - allows all internet access), OR
- "Specified domains" and add
api.spotify.com(more secure - Spotify only)
Why?
- The skill makes HTTP requests to
api.spotify.com - Without network egress, all API calls will be blocked
- This security feature must be explicitly enabled
Verification:
- Toggle should be blue/ON
- For "All domains": Message shows "Claude can access all domains on the internet"
- For "Specified domains": Verify
api.spotify.comis in the domain list
Setting Up Your Spotify App
Step 1: Create a Spotify Developer Account
1. Go to https://developer.spotify.com/dashboard 2. Log in with your Spotify account (or create one) 3. Accept the terms and create your developer account
Step 2: Create an Application
1. Click "Create an App" 2. Enter app name and description 3. Accept the terms of service 4. Click "Create" 5. Agree to Spotify API terms
Step 3: Get Your Credentials
After creating your app, you'll have:
- Client ID: Unique identifier for your app
- Client Secret: Secret key for authentication (keep private!)
- Redirect URI: URL where Spotify redirects after user authorization
Example:
Client ID: 1234567890abcdef1234567890abcdef
Client Secret: fedcba0987654321fedcba0987654321
Redirect URI: http://localhost:8888/callbackStep 4: Set Redirect URI
1. In your app settings, click "Edit Settings" 2. Add your redirect URI under "Redirect URIs" 3. Examples:
- Development:
http://localhost:8888/callback - Production:
https://yourapp.com/callback
OAuth 2.0 Flow
Authorization Code Flow (User Authentication)
This is the recommended flow for accessing user data.
Step 1: Request User Authorization
Direct user to Spotify's authorization endpoint:
GET https://accounts.spotify.com/authorize
Parameters:
- client_id: Your Client ID
- response_type: "code"
- redirect_uri: Your registered redirect URI
- scope: Space-separated list of scopes
- show_dialog: "true" (optional, forces re-authentication)Example URL:
https://accounts.spotify.com/authorize?
client_id=1234567890abcdef1234567890abcdef&
response_type=code&
redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fcallback&
scope=playlist-modify-public%20playlist-modify-private&
show_dialog=trueStep 2: User Authorizes
User sees Spotify login and permission request screen. After authorizing, Spotify redirects to your redirect URI with authorization code:
http://localhost:8888/callback?code=AQBPa...&state=xyzStep 3: Exchange Code for Access Token
Make a POST request to get access token:
POST https://accounts.spotify.com/api/token
Headers:
Authorization: Basic {base64(client_id:client_secret)}
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=authorization_code&
code={authorization_code}&
redirect_uri={redirect_uri}Example using curl:
curl -H "Authorization: Basic $(echo -n 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' | base64)" \
-d "grant_type=authorization_code&code=AQBPa...&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fcallback" \
https://accounts.spotify.com/api/tokenStep 4: Receive Access Token
Response includes:
{
"access_token": "NgCXRK...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "NgCXRK...",
"scope": "playlist-modify-public playlist-modify-private"
}Store the refresh_token for later token renewal.
Client Credentials Flow (Backend Authentication)
For backend operations without user context:
POST https://accounts.spotify.com/api/token
Headers:
Authorization: Basic {base64(client_id:client_secret)}
Content-Type: application/x-www-form-urlencoded
Body:
grant_type=client_credentialsReturns access token (no refresh token) valid for 1 hour.
Credential Management
Environment Variables
Store credentials as environment variables for security:
export SPOTIFY_CLIENT_ID="your_client_id"
export SPOTIFY_CLIENT_SECRET="your_client_secret"
export SPOTIFY_REDIRECT_URI="http://localhost:8888/callback"
export SPOTIFY_ACCESS_TOKEN="your_access_token"
export SPOTIFY_REFRESH_TOKEN="your_refresh_token"Python Implementation
import os
from spotify_client import SpotifyClient
# Load from environment
client = SpotifyClient(
client_id=os.getenv("SPOTIFY_CLIENT_ID"),
client_secret=os.getenv("SPOTIFY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIFY_REDIRECT_URI"),
access_token=os.getenv("SPOTIFY_ACCESS_TOKEN"),
refresh_token=os.getenv("SPOTIFY_REFRESH_TOKEN")
)
# Use client for API calls
playlists = client.get_user_playlists()Secure Credential Storage
Development:
- Use
.envfile with python-dotenv - Never commit credentials to version control
- Add
.envto.gitignore
Production:
- Use environment variables
- Use secret management services (AWS Secrets Manager, HashiCorp Vault, etc.)
- Rotate refresh tokens periodically
- Implement rate limiting and monitoring
Example: Initial OAuth Setup
import os
import webbrowser
from spotify_client import SpotifyClient
# 1. Initialize client with credentials only
client = SpotifyClient(
client_id=os.getenv("SPOTIFY_CLIENT_ID"),
client_secret=os.getenv("SPOTIFY_CLIENT_SECRET"),
redirect_uri=os.getenv("SPOTIFY_REDIRECT_URI")
)
# 2. Get authorization URL
auth_url = client.get_authorization_url()
print(f"Visit this URL to authorize: {auth_url}")
webbrowser.open(auth_url)
# 3. User visits URL, grants permissions, and is redirected
# They'll be redirected to http://localhost:8888/callback?code=...
# 4. Extract authorization code from redirect URL and exchange
auth_code = input("Enter the authorization code from the redirect URL: ")
token_data = client.get_access_token(auth_code)
print(f"Access Token: {token_data['access_token']}")
print(f"Refresh Token: {token_data['refresh_token']}")
print(f"Expires in: {token_data['expires_in']} seconds")
# 5. Store refresh token for future use
os.environ["SPOTIFY_REFRESH_TOKEN"] = token_data["refresh_token"]Token Management
Token Expiration
- Access Token: Expires in 3600 seconds (1 hour)
- Refresh Token: Does not expire (unless revoked by user)
Automatic Token Refresh
The SpotifyClient automatically refreshes expired tokens:
# No manual refresh needed - client handles it automatically
playlists = client.get_user_playlists() # Checks and refreshes if neededManual Token Refresh
# Manually refresh if needed
token_data = client.refresh_access_token()
print(f"New access token: {token_data['access_token']}")Checking Token Status
import time
# Check if token needs refresh
if client.token_expires_at and time.time() >= client.token_expires_at - 60:
print("Token expiring soon or already expired")
client.refresh_access_token()
else:
print("Token is valid")Scopes Explained
Scopes determine what data and actions are allowed for your app.
Playlist Scopes
| Scope | Access |
|---|---|
playlist-read-private | Read private playlists |
playlist-read-collaborative | Read collaborative playlists |
playlist-modify-public | Create/modify public playlists |
playlist-modify-private | Create/modify private playlists |
ugc-image-upload | Upload custom playlist cover images ⚠️ Required for cover art generation! |
User Scopes
| Scope | Access |
|---|---|
user-read-private | Read user private profile data |
user-read-email | Read user email address |
user-read-private | Read user's private profile information |
Library Scopes
| Scope | Access |
|---|---|
user-library-read | Read saved tracks/albums |
user-library-modify | Save/unsave tracks/albums |
Listening History
| Scope | Access |
|---|---|
user-top-read | Read user's top tracks/artists |
user-read-recently-played | Read recently played tracks |
Playback Scopes
| Scope | Access |
|---|---|
user-read-currently-playing | Read currently playing track |
user-read-playback-state | Read playback state |
user-modify-playback-state | Control playback |
Recommended Scope Combinations
For Playlist Management (without cover art):
playlist-modify-public
playlist-modify-private
user-library-readFor Playlist Management WITH Cover Art Generation:
playlist-modify-public
playlist-modify-private
user-library-read
ugc-image-upload⚠️ Important: The ugc-image-upload scope is required to upload custom cover art images to playlists!
For Full User Experience:
playlist-modify-public
playlist-modify-private
user-library-read
user-library-modify
user-read-private
user-read-email
user-top-read
user-read-currently-playing
user-modify-playback-state
user-read-playback-state
ugc-image-uploadRequesting Specific Scopes
# Request specific scopes
scopes = [
"playlist-modify-public",
"playlist-modify-private",
"user-library-read"
]
auth_url = client.get_authorization_url(scope=scopes)Security Best Practices
1. Never expose Client Secret - Keep it secret like a password 2. Use HTTPS - Always use HTTPS for redirect URIs in production 3. Rotate tokens - Implement token rotation for long-running processes 4. Validate redirects - Verify redirect URI before processing auth code 5. Rate limiting - Implement rate limiting to prevent abuse 6. Error handling - Don't expose credential details in error messages 7. Monitor access - Log and monitor API usage for suspicious activity 8. Secure storage - Use encrypted storage for sensitive credentials
Troubleshooting
Invalid Client ID / Secret
- Verify credentials match your app settings
- Check for accidental whitespace
- Regenerate credentials if needed
Invalid Redirect URI
- Exact match required (including http/https)
- URLs are case-sensitive
- Check URL encoding in requests
Unauthorized (401)
- Access token may be expired - refresh it
- Token may have been revoked by user
- Check token scope has required permissions
Forbidden (403)
- Insufficient scopes requested
- User hasn't authorized the action
- May require additional permissions
Rate Limited (429)
- Check
Retry-Afterheader - Implement exponential backoff
- Batch requests when possible
Spotify Playlist Cover Art Generator - LLM Execution Guide
⚠️ CRITICAL: Output Format Limitation
The cover art is generated as SVG and immediately converted to PNG (static image) for Spotify upload.
- ✅ SVG features that work: Gradients, colors, text, shapes, patterns
- ❌ SVG features that DON'T work: Animations (
<animate>,<animateTransform>) - these are lost in PNG conversion - 📊 Final format: Static PNG image (640x640 pixels)
DO NOT use SVG animations - they will not be visible in the final uploaded cover art.
---
Overview
This guide enables you to create professional Spotify playlist cover art by analyzing the playlist's actual content (tracks, artists, genres) to determine appropriate colors, styles, and mood. You are self-contained and don't need external theme lists or color presets.
Core Principle: Content-Driven Design
You will determine colors and style by: 1. Analyzing the playlist's tracks and artists using the Spotify API 2. Extracting genre, mood, and energy from the actual music 3. Applying color psychology based on the content analysis 4. Creating unique, contextually appropriate designs
This makes you truly autonomous - able to handle any playlist without needing predefined themes.
---
Quick Reference: Complete Workflow Examples
Example 1: High-Energy Workout Playlist
User Request: "Create cover art for my playlist '7i9dQZF1DXaXB8fQg7xif'"
# Step 1: Get Playlist Content
playlist = client.get_playlist("7i9dQZF1DXaXB8fQg7xif")
playlist_name = playlist['name'] # "Beast Mode Gym"
tracks = playlist['tracks']['items']
artists = ["Metallica", "Rage Against the Machine", "Linkin Park"]
# Step 2: Analyze Content
# Detected: High-energy rock/metal, Energy: 9/10, Mood: Aggressive/Intense
# Step 3: Determine Colors
gradient_start = "#E63946" # Intense red (rock/metal + high energy)
gradient_end = "#1D1D1D" # Almost black (metal aesthetic)
text_color = "#FFFFFF" # Maximum contrast
# Step 4: Generate
from scripts.cover_art_generator import CoverArtGenerator
generator = CoverArtGenerator(client)
png = generator.generate_cover_art(
title="BEAST MODE GYM",
gradient_start="#E63946",
gradient_end="#1D1D1D",
text_color="#FFFFFF",
size=640
)
# Step 5: Upload
generator.upload_cover_image(playlist_id, png)Result: Bold red-to-black gradient with large white text, perfect for gym visibility ✓
---
Example 2: Chill Ambient Playlist
User Request: "Songs for getting sleepy"
# Analysis: Genre=ambient, Energy: 2/10, Mood: Calm/Relaxing
# Colors (from Step 2 color psychology)
gradient_start = "#4ECDC4" # Cool teal (chill, calming)
gradient_end = "#556FB5" # Soft blue (peaceful)
text_color = "#FFFFFF" # Clean contrast
# Generate
png = generator.generate_cover_art(
title="SONGS FOR GETTING SLEEPY",
gradient_start="#4ECDC4",
gradient_end="#556FB5",
text_color="#FFFFFF",
size=640
)Result: Soothing blue gradient with clear white text ✓
---
Example 3: Vague Request Handling
User Request: "My Favorite Songs"
Your Response:
"I'd love to create cover art for your playlist! To make it perfect, could you tell me:
- What genres of music are included? (rock, pop, hip-hop, electronic, etc.)
- What's the vibe or mood? (energetic, chill, nostalgic, romantic, etc.)
- Any particular era or style you're going for? (modern, 80s, vintage, etc.)
This will help me choose the right colors, fonts, and design elements."After user responds: "It's mostly indie rock and alternative from the 2000s, pretty chill and nostalgic vibe"
# Analysis: Genre=indie/alternative, Era=2000s, Energy: 5/10, Mood: Nostalgic/Chill
# Colors (warm, vintage feel for nostalgia)
gradient_start = "#D4A574" # Vintage gold
gradient_end = "#6B4423" # Warm brown
text_color = "#F5F5DC" # Cream (softer than white)
png = generator.generate_cover_art(
title="MY FAVORITE SONGS",
gradient_start="#D4A574",
gradient_end="#6B4423",
text_color="#F5F5DC",
size=640
)Result: Warm vintage aesthetic matching 2000s indie nostalgia ✓
---
Step-by-Step Execution Process
Step 1: Analyze Playlist Content
FIRST: Get the actual playlist data
from scripts.spotify_client import SpotifyClient
client = SpotifyClient(access_token="...")
# Get playlist details and tracks
playlist_info = client.get_playlist(playlist_id)
playlist_name = playlist_info['name']
tracks = playlist_info['tracks']['items']
# Extract key information
artists = [track['track']['artists'][0]['name'] for track in tracks if track['track']]
track_names = [track['track']['name'] for track in tracks if track['track']]ANALYZE THE CONTENT:
- Genres: What genres do these artists represent?
- Era/Time Period: What decades are these tracks from?
- Energy Level: Are these high-energy or calm tracks?
- Mood: Aggressive, romantic, melancholic, upbeat?
- Common Themes: Workout, sleep, party, focus, nostalgia?
EXTRACT KEY CHARACTERISTICS:
Based on playlist content:
- Primary Genre: [determined from artists/tracks]
- Energy Level: [1-10 scale based on track analysis]
- Dominant Mood: [extracted from track names, artists, genres]
- Era/Style: [determined from artist eras]
- Keywords: [2-3 most important words from playlist name]HANDLING VAGUE PLAYLIST NAMES:
If the playlist name is generic (e.g., "My Favorite Songs", "My Playlist", "Good Music"), use the content analysis to understand the theme, then ASK clarifying questions:
"I analyzed your playlist and see it contains [genres/artists].
What vibe should the cover art convey? (energetic, chill, nostalgic, etc.)"Vague Request Indicators:
- Generic terms: "My Favorite Songs", "My Playlist", "Good Music"
- No genre indicators: "Sunday Vibes", "Mood Music", "The Mix"
- Personal/subjective only: "Songs I Love", "Best Ever", "My Jams"
- Single ambiguous words: "Vibes", "Feels", "Energy"
Required Clarifying Questions (ask ALL that apply):
IF playlist_name_is_vague AND insufficient_context THEN
ASK: "What genre(s) of music are in this playlist? (rock, pop, electronic, etc.)"
ASK: "What's the mood or context? (workout, relaxation, party, study, etc.)"
ASK: "Any specific era or style? (80s, modern, vintage, etc.)"
WAIT: for_user_response
THEN: proceed_with_analysis---
Step 2: Determine Colors from Content Analysis
USE COLOR PSYCHOLOGY based on your Step 1 analysis:
Energy Level Mapping:
- High Energy (8-10): Bright, saturated colors
- Examples: Bright red (#E63946), electric orange (#FF6B35), hot pink (#FF6B9D)
- Medium Energy (4-7): Balanced, clear colors
- Examples: Deep blue (#0369A1), vibrant purple (#8B5DFF), teal (#4ECDC4)
- Low Energy (1-3): Muted, soft colors
- Examples: Soft blue (#A8D8FF), pale green (#B2DFDB), light gray (#E8E8E8)
Genre-to-Color Mapping:
Use these associations when you detect genres from the playlist:
| Genre Detected | Color Scheme (gradient_start, gradient_end, text_color) |
|---|---|
| Rock/Metal | Intense red + Black (#E63946, #1D1D1D, #FFFFFF) |
| Electronic/EDM | Neon colors (#00FFF0, #8B5DFF, #FFFFFF) |
| Hip-Hop/Rap | Purple + Gold (#6A4C93, #FFD93D, #FFFFFF) |
| Jazz/Blues | Brown + Cream (#6B4423, #F5F5DC, #2C2C2C) |
| Classical | Gold + Navy (#D4A574, #2C3E50, #F5F5DC) |
| Pop | Pink + Yellow (#FF6B9D, #FFD93D, #FFFFFF) |
| Country | Earth tones (#D4A574, #8B4513, #F5F5DC) |
| Reggae | Green + Yellow (#90BE6D, #FFD93D, #1A1A1A) |
| Indie/Alt | Muted pastels (#B2DFDB, #E8B4B8, #2C3E50) |
| Ambient/Chill | Cool blues (#4ECDC4, #556FB5, #FFFFFF) |
Mood-to-Color Mapping:
| Mood Detected | Color Temperature | Saturation | Example Scheme |
|---|---|---|---|
| Aggressive | Warm | High | Red + Black |
| Calm/Peaceful | Cool | Low | Soft blue + Light purple |
| Happy/Upbeat | Warm | High | Yellow + Orange |
| Melancholic | Cool | Low | Gray + Deep blue |
| Romantic | Warm | Medium | Pink + Rose |
| Nostalgic | Warm | Low | Sepia + Vintage gold |
| Energetic | Warm/Bright | High | Orange + Red |
| Focus/Study | Cool | Low | Light blue + White |
Color Psychology Reference:
Color Temperature:
- Warm (energetic, passionate): Reds, oranges, yellows
- Cool (calm, focused): Blues, teals, purples
- Neutral (balanced): Grays, beige, muted tones
Saturation:
- High saturation: Energetic, bold, attention-grabbing
- Medium saturation: Balanced, professional
- Low saturation: Calm, sophisticated, subtle
Contrast:
- High contrast: Dramatic, easier to read (rock, workout, party)
- Low contrast: Softer, more subtle (ambient, classical, sleep)
Common Color Meanings:
- Red: Energy, passion, intensity, aggression
- Orange: Enthusiasm, creativity, warmth
- Yellow: Happiness, optimism, attention
- Green: Nature, calm, growth, balance
- Blue: Trust, calm, focus, professionalism
- Purple: Creativity, luxury, electronic/synthetic
- Pink: Romance, softness, pop music
- Brown: Earth, vintage, acoustic, organic
- Black: Power, sophistication, mystery
- White: Clean, minimal, modern, space
SYNTHESIZE YOUR COLOR SCHEME:
1. Determine primary mood from playlist content 2. Select base colors from genre/mood mappings 3. Adjust saturation based on energy level 4. Ensure high contrast for text readability (minimum 4.5:1 ratio)
Example Analysis:
Playlist: "Ultimate Workout Mix"
Content Analysis:
- Artists: Eminem, Metallica, Linkin Park, Rage Against the Machine
- Genres Detected: Rock, Metal, Hip-Hop
- Energy Level: 9/10 (very high energy)
- Mood: Aggressive, Intense, Motivational
Color Decision:
gradient_start = "#E63946" # Intense red (high energy, aggressive)
gradient_end = "#1D1D1D" # Almost black (metal/rock aesthetic)
text_color = "#FFFFFF" # Maximum contrast
Reasoning: High-energy rock/metal requires bold, aggressive colors
with maximum contrast for workout/gym visibility.Multi-Genre Playlists:
For playlists with mixed genres, blend color schemes:
# Example: Playlist with Electronic (60%) + Chill (40%) tracks
# Electronic = bright neon colors
# Chill = cool, muted tones
# Blend = Teal to Purple gradient
gradient_start = "#4ECDC4" # Cool teal (chill influence)
gradient_end = "#8B5DFF" # Electric purple (electronic influence)
text_color = "#FFFFFF" # Clean contrastEra-Specific Colors:
If tracks are primarily from specific eras:
80s/90s Detected:
- Neon colors (cyan, magenta, yellow)
- High saturation
- Example: "#00FFF0" to "#FF00FF"
Modern/Contemporary:
- Clean, simple gradients
- Muted or monochrome
- Example: "#F5F5F5" to "#2C2C2C"
Classic/Vintage (pre-1980):
- Warm, faded tones
- Lower saturation
- Example: "#D4A574" to "#6B4423"
---
Step 3: Typography Rules (CRITICAL - NON-NEGOTIABLE)
Font Sizes for Thumbnail Readability:
1. Primary word: 70-90px (LARGE) 2. Secondary word: 60-80px 3. Supporting words: 40-60px 4. NEVER use fonts smaller than 40px
Text Wrapping (for long titles):
Long playlist names must wrap properly to maintain readability. The cover_art_generator.py script includes automatic text wrapping:
# Automatic text wrapping at word boundaries
def wrap_text(text, max_chars=20):
words = text.split()
lines = []
current_line = []
current_length = 0
for word in words:
if current_length + len(word) + len(current_line) > max_chars:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
current_length = len(word)
else:
current_line.append(word)
current_length += len(word)
if current_line:
lines.append(' '.join(current_line))
return linesText Wrapping Strategy:
- Max characters per line: 20 (for readability)
- Break at word boundaries: Never split words
- Line height: 110% of font size (font_size * 1.1)
- Example: "My Ultimate Workout Power Hour" → ["My Ultimate Workout", "Power Hour"]
Text Layout:
- Text occupies 80% of canvas width
- Line height: 110% of font size (font_size * 1.1)
- Vertical spacing: 20px between elements
- Center alignment (both horizontal and vertical)
Font Weight:
- Titles: Bold (weight: 700-900)
- Subtitles: Semi-bold (weight: 600)
Edge Cases:
Very Long Titles (>25 characters):
Strategy 1: Multi-line wrapping (primary method)
title_lines = wrap_text("My Ultimate Workout Power Hour", max_chars=20)
# Result: ["My Ultimate Workout", "Power Hour"]Strategy 2: Abbreviate common words (if still too long)
title = title.replace("The ", "").replace(" and ", " & ")
# "The Best Songs and More" → "Best Songs & More"Strategy 3: Focus on keywords (last resort)
# "My Favorite Classic Rock Songs" → "CLASSIC ROCK"
keywords = extract_keywords(title)Special Characters:
- Emojis: Replace with text or remove (e.g., "🎵 Music" → "Music")
- Symbols (&, @, #): Keep if part of title, encode properly for SVG
- Non-English: Ensure font supports character set
---
Step 4: Generate the Cover Art
EXECUTE THIS CODE:
from scripts.cover_art_generator import CoverArtGenerator
from scripts.spotify_client import SpotifyClient
# Initialize
client = SpotifyClient(access_token="...")
generator = CoverArtGenerator(client)
# Generate with colors determined from Step 2
png_path = generator.generate_cover_art(
title="Workout Power", # From playlist name
subtitle="", # Optional
gradient_start="#E63946", # Determined from Step 2
gradient_end="#1D1D1D", # Determined from Step 2
text_color="#FFFFFF", # High contrast
output_path="workout_cover.png",
size=640 # Spotify recommended
)
print(f"✓ Cover art created: {png_path}")
# Upload to playlist
success = generator.upload_cover_image(
playlist_id="37i9dQZF1DXaXB8fQg7xif",
image_path=png_path
)
if success:
print("✓ Cover art uploaded to Spotify!")
else:
print("✗ Upload failed - check authentication and permissions")Key Parameters:
title: The main text (required)subtitle: Optional secondary textgradient_start: Top gradient color from Step 2 analysisgradient_end: Bottom gradient color from Step 2 analysistext_color: Text color ensuring 4.5:1 contrast ratiooutput_path: Where to save the PNG filesize: Image dimensions (640x640 recommended for Spotify)
---
Step 5: Quality Assurance Checklist
Before finalizing, verify:
Readability:
- [ ] Can you read the title at 100x100px (thumbnail size)?
- [ ] Is there sufficient contrast between text and background (≥4.5:1 ratio)?
- [ ] Does text wrap properly (no cutoffs)?
- [ ] Are all words clearly visible?
Design Quality:
- [ ] Do colors match the playlist content/mood?
- [ ] Is the gradient direction visually appealing?
- [ ] Does it look professional and polished?
- [ ] Would you click on this in Spotify?
Technical Requirements:
- [ ] Image is 640x640px or larger
- [ ] File size < 256KB (Spotify limit)
- [ ] Format is JPEG or PNG
- [ ] Colors are vibrant but not oversaturated
Accessibility (WCAG 2.1 AA):
- [ ] Contrast ratio ≥ 4.5:1 for normal text
- [ ] Contrast ratio ≥ 3:1 for large text (>24px)
- [ ] Readable for colorblind users (test with grayscale)
Content Alignment:
- [ ] Colors reflect the actual music genres in the playlist
- [ ] Energy level matches the track content
- [ ] Mood is appropriate for the use case (workout, sleep, party, etc.)
---
Advanced Techniques
Empty or Private Playlists
If you cannot access playlist tracks:
1. Ask for genre/mood: "I can't access the playlist tracks. What genre and mood should the cover art convey?" 2. Use playlist name only: Extract clues from the name itself 3. Default to universal design: Use balanced, professional colors
Error Recovery
If generation fails: 1. Check that all color codes are valid hex format (#RRGGBB) 2. Ensure title is not empty 3. Verify gradient_start and gradient_end are different colors 4. Confirm text_color has sufficient contrast with both gradient colors
If upload fails: 1. Verify the ugc-image-upload scope is included in OAuth token 2. Check that image is <256KB 3. Ensure playlist is owned by the authenticated user 4. Confirm image is valid PNG or JPEG format
Template Fallback (Alternative Method)
If content analysis is unavailable, use these predefined templates:
Rock/Metal Template:
gradient_start = "#E63946" # Intense red
gradient_end = "#1D1D1D" # Almost black
text_color = "#FFFFFF" # WhiteElectronic/EDM Template:
gradient_start = "#00FFF0" # Neon cyan
gradient_end = "#8B5DFF" # Electric purple
text_color = "#FFFFFF" # WhiteChill/Ambient Template:
gradient_start = "#4ECDC4" # Cool teal
gradient_end = "#556FB5" # Soft blue
text_color = "#FFFFFF" # WhitePop/Upbeat Template:
gradient_start = "#FF6B9D" # Hot pink
gradient_end = "#FFD93D" # Bright yellow
text_color = "#FFFFFF" # WhiteClassical/Jazz Template:
gradient_start = "#D4A574" # Gold
gradient_end = "#2C3E50" # Navy
text_color = "#F5F5DC" # Cream---
Summary
You are now self-contained and autonomous for cover art generation. You don't need preset theme lists - instead, you:
1. Analyze the playlist's actual content (tracks, artists, genres) 2. Extract genre, mood, and energy from the music 3. Apply color psychology based on your analysis 4. Generate contextually appropriate cover art 5. Verify quality and accessibility
This approach makes you adaptive to any playlist - past, present, or future - by analyzing the actual content rather than relying on predefined themes.
Decision Tree
START
├─ Can access playlist tracks?
│ ├─ YES → Analyze content (Step 1) → Determine colors (Step 2)
│ └─ NO → Ask user for genre/mood → Use template fallback
│
├─ Is playlist name vague?
│ ├─ YES → Ask clarifying questions → Wait for response
│ └─ NO → Extract keywords from name
│
├─ Colors determined?
│ ├─ YES → Apply typography rules (Step 3)
│ └─ NO → Use template fallback
│
├─ Title > 25 characters?
│ ├─ YES → Apply text wrapping
│ └─ NO → Use title as-is
│
├─ Generate cover art (Step 4)
│
├─ Quality checks pass? (Step 5)
│ ├─ YES → Upload to Spotify
│ └─ NO → Adjust colors/typography → Regenerate
│
ENDSuccess Metrics
Good Cover Art:
- ✓ Colors derived from actual playlist content
- ✓ Readable at thumbnail size (100x100px)
- ✓ Colors match playlist mood/energy
- ✓ Professional, polished appearance
- ✓ High contrast for accessibility (≥4.5:1 ratio)
- ✓ Unique and contextually appropriate
Poor Cover Art:
- ✗ Generic colors not based on content
- ✗ Text too small or cramped
- ✗ Low contrast (hard to read)
- ✗ Colors don't match genre/mood
- ✗ Looks amateurish or cluttered
Final Checklist
Before delivering cover art to the user:
1. [ ] Analyzed playlist content (tracks, artists, genres) 2. [ ] Extracted energy level and mood from content 3. [ ] Determined colors using color psychology 4. [ ] Applied proper typography (fonts 40-90px) 5. [ ] Implemented text wrapping for long titles 6. [ ] Verified readability at thumbnail size 7. [ ] Checked contrast ratios (WCAG 2.1 AA ≥4.5:1) 8. [ ] Generated image at 640x640px 9. [ ] Uploaded to Spotify successfully 10. [ ] Confirmed visual quality matches content
---
Troubleshooting
Common Issues
"401 Unauthorized" when uploading:
- Missing
ugc-image-uploadscope in OAuth token - Solution: Re-run
get_refresh_token.pyto get new token with all scopes
"Image too large" error:
- File size exceeds 256KB
- Solution: Reduce image size or quality
Text is unreadable in thumbnail:
- Font size too small (<40px)
- Solution: Increase font size or use text wrapping
Colors don't match playlist mood:
- Insufficient content analysis
- Solution: Review Step 1 and Step 2 more carefully
Text gets cut off:
- Title too long without wrapping
- Solution: Apply text wrapping (Step 3)
---
Remember: The primary method is content-driven analysis (Steps 1-2). Only use template fallback when content is unavailable. Always prioritize readability and accessibility over artistic complexity.
# Spotify API Skill - Python Dependencies
#
# Install with: pip install -r requirements.txt
#
# Minimum Python version: 3.8+
# HTTP requests for Spotify Web API
requests>=2.31.0
# Environment variable management from .env files
python-dotenv>=1.0.0
# Cover art generation dependencies
cairosvg>=2.7.0
pillow>=10.0.0
"""
Spotify Cover Art Generator
Generates custom SVG-based cover art and uploads to Spotify playlists.
IMPORTANT: This tool is designed to be used with the LLM execution guide.
See: spotify-api/references/COVER_ART_LLM_GUIDE.md
The guide provides comprehensive instructions for:
- Analyzing playlist content to determine appropriate colors
- Applying color psychology based on genre, mood, and energy
- Creating contextually appropriate designs without preset themes
- Ensuring accessibility and professional quality
Color Selection Approaches:
1. Content-Driven (RECOMMENDED): Analyze playlist tracks/artists using the guide
2. Preset Themes (LEGACY): Use predefined theme/genre/artist colors below
3. Custom Colors: Specify exact RGB values
For best results, follow the LLM guide to analyze playlist content and
determine colors using color psychology rather than relying on presets.
"""
import os
import base64
import requests
from io import BytesIO
from typing import Dict, List, Optional, Tuple
try:
import cairosvg
from PIL import Image
except ImportError as e:
print("Missing required dependencies. Install with:")
print("pip install cairosvg pillow")
raise e
# ============================================================================
# LEGACY PRESETS (For backward compatibility)
# ============================================================================
# These preset color schemes are provided for quick usage, but the RECOMMENDED
# approach is to analyze playlist content using the LLM guide and determine
# colors based on:
# - Actual genres detected from tracks/artists
# - Energy level analysis (1-10 scale)
# - Mood extracted from content
# - Color psychology principles
#
# See: spotify-api/references/COVER_ART_LLM_GUIDE.md for the content-driven approach
# ============================================================================
# Preset theme color schemes (gradient_start, gradient_end, text_color)
THEME_COLORS: Dict[str, Tuple[str, str, str]] = {
# Mood-based themes
"summer": ("#FFD93D", "#FF6B9D", "#FFFFFF"),
"chill": ("#4ECDC4", "#556FB5", "#FFFFFF"),
"energetic": ("#FF6B35", "#F72C25", "#FFFFFF"),
"dark": ("#8B5DFF", "#1A1A2E", "#FFFFFF"),
"vintage": ("#D4A574", "#6B4423", "#F5F5DC"),
"neon": ("#00FFF0", "#FF00FF", "#FFFFFF"),
"minimal": ("#F5F5F5", "#2C2C2C", "#1A1A1A"),
"warm": ("#FFAD60", "#FF6C40", "#FFFFFF"),
"cool": ("#A8D8FF", "#3A6EA5", "#FFFFFF"),
"sunset": ("#FF8C42", "#6A4C93", "#FFFFFF"),
"ocean": ("#00B4D8", "#03045E", "#FFFFFF"),
"forest": ("#90BE6D", "#2D6A4F", "#FFFFFF"),
# Additional mood themes
"romantic": ("#FF6B9D", "#C44569", "#FFFFFF"),
"melancholic": ("#6C5B7B", "#355C7D", "#E8E8E8"),
"euphoric": ("#FFC837", "#FF8008", "#FFFFFF"),
"peaceful": ("#B2DFDB", "#80CBC4", "#2C3E50"),
"intense": ("#C0392B", "#8E44AD", "#FFFFFF"),
"dreamy": ("#A8E6CF", "#FFD3B6", "#FFFFFF"),
"nostalgic": ("#E8B4B8", "#9C7A97", "#F5F5DC"),
"party": ("#F857A6", "#FF5858", "#FFFFFF"),
}
# Genre-based color schemes
GENRE_COLORS: Dict[str, Tuple[str, str, str]] = {
"rock": ("#E63946", "#1D1D1D", "#FFFFFF"),
"pop": ("#FF006E", "#8338EC", "#FFFFFF"),
"jazz": ("#FFB703", "#023047", "#FFFFFF"),
"classical": ("#F8F9FA", "#6A040F", "#1A1A1A"),
"electronic": ("#00F5FF", "#0077B6", "#FFFFFF"),
"hip-hop": ("#FB5607", "#03071E", "#FFFFFF"),
"country": ("#DDA15E", "#6A4C2A", "#FFFFFF"),
"indie": ("#FF6B6B", "#4ECDC4", "#FFFFFF"),
"metal": ("#ADB5BD", "#212529", "#FFFFFF"),
"r&b": ("#9D4EDD", "#5A189A", "#FFFFFF"),
"blues": ("#2E4057", "#048BA8", "#E8E8E8"),
"folk": ("#A27E52", "#5D4E37", "#F5F5DC"),
"reggae": ("#FFD700", "#228B22", "#1A1A1A"),
"punk": ("#FF1744", "#000000", "#FFFFFF"),
"soul": ("#8E24AA", "#D81B60", "#FFFFFF"),
}
# Artist/Band-specific moods (can be used for artist playlists)
ARTIST_MOODS: Dict[str, Tuple[str, str, str]] = {
"beatles": ("#FFD700", "#FF6347", "#FFFFFF"), # Sunny, British Invasion
"pinkfloyd": ("#1A1A2E", "#8B5DFF", "#FFFFFF"), # Dark, psychedelic
"radiohead": ("#2C3E50", "#34495E", "#ECF0F1"), # Moody, alternative
"queen": ("#FFD700", "#8B0000", "#FFFFFF"), # Regal, theatrical
"nirvana": ("#95A5A6", "#34495E", "#ECF0F1"), # Grunge, raw
"davidbowie": ("#FF6B9D", "#4169E1", "#FFFFFF"), # Glam, eclectic
"ledzeppelin": ("#D4AF37", "#8B4513", "#FFFFFF"), # Golden, classic rock
"acdc": ("#FF0000", "#000000", "#FFFFFF"), # Electric, hard rock
"therollingstones": ("#C0392B", "#2C3E50", "#FFFFFF"), # Raw, rebellious
"fleetwoodmac": ("#E8B4B8", "#9C7A97", "#F5F5DC"), # Warm, classic
}
class CoverArtGenerator:
"""
Generate and upload custom cover art for Spotify playlists.
Attributes:
client_id: Spotify application client ID
client_secret: Spotify application client secret
access_token: Valid Spotify user access token
"""
def __init__(self, client_id: str, client_secret: str, access_token: str):
"""
Initialize cover art generator.
Args:
client_id: Spotify application client ID
client_secret: Spotify application client secret
access_token: Valid Spotify user access token with playlist-modify scope
"""
self.client_id = client_id
self.client_secret = client_secret
self.access_token = access_token
self.base_url = "https://api.spotify.com/v1"
def create_and_upload_cover(
self,
playlist_id: str,
title: str,
subtitle: str = "",
theme: Optional[str] = None,
genre: Optional[str] = None,
artist: Optional[str] = None,
gradient_start: Optional[str] = None,
gradient_end: Optional[str] = None,
text_color: Optional[str] = None
) -> bool:
"""
Generate cover art and upload to Spotify playlist in one step.
Args:
playlist_id: Spotify playlist ID
title: Main title text (large font, readable at thumbnail size)
subtitle: Subtitle text (smaller font, optional)
theme: Preset theme name (e.g., 'summer', 'chill', 'dark', 'romantic')
genre: Genre-based color scheme (e.g., 'rock', 'jazz', 'pop', 'blues')
artist: Artist/band name for artist-specific moods (e.g., 'beatles', 'pinkfloyd')
gradient_start: Custom gradient start color (hex, e.g., '#FF6B6B')
gradient_end: Custom gradient end color (hex)
text_color: Custom text color (hex)
Returns:
True if successful, False otherwise
Example:
>>> generator = CoverArtGenerator(client_id, client_secret, token)
>>> generator.create_and_upload_cover(
... playlist_id="37i9dQZF1DXcBWIGoYBM5M",
... title="Summer Vibes",
... subtitle="Feel Good Hits",
... theme="summer"
... )
True
"""
try:
# Generate cover art PNG
png_path = self.generate_cover_art(
title=title,
subtitle=subtitle,
theme=theme,
genre=genre,
artist=artist,
gradient_start=gradient_start,
gradient_end=gradient_end,
text_color=text_color,
output_path=None # Use temp file
)
# Upload to Spotify
success = self.upload_cover_image(playlist_id, png_path)
# Clean up temp file
if os.path.exists(png_path):
os.remove(png_path)
return success
except Exception as e:
print(f"Error creating and uploading cover: {e}")
return False
def generate_cover_art(
self,
title: str,
subtitle: str = "",
theme: Optional[str] = None,
genre: Optional[str] = None,
artist: Optional[str] = None,
gradient_start: Optional[str] = None,
gradient_end: Optional[str] = None,
text_color: Optional[str] = None,
output_path: Optional[str] = None,
size: int = 600
) -> str:
"""
Generate cover art image (SVG → PNG).
Typography optimized for thumbnail readability at 80% width.
RECOMMENDED WORKFLOW:
1. Use COVER_ART_LLM_GUIDE.md to analyze playlist content
2. Determine colors from actual tracks/artists/genres
3. Apply color psychology based on energy and mood
4. Pass custom colors (gradient_start, gradient_end, text_color)
This method supports three approaches:
- Content-Driven (RECOMMENDED): Use guide to analyze and pass custom colors
- Legacy Presets: Use theme/genre/artist parameters for preset colors
- Direct Colors: Specify exact gradient_start, gradient_end, text_color
Args:
title: Main title text (will be large and readable)
subtitle: Subtitle text (optional)
theme: Preset theme name (mood-based)
genre: Genre-based color scheme
artist: Artist/band name for artist-specific moods
gradient_start: Custom gradient start color
gradient_end: Custom gradient end color
text_color: Custom text color
output_path: Output PNG file path (None = temp file)
size: Output size in pixels (default 600x600)
Returns:
Path to generated PNG file
Raises:
ValueError: If no color scheme is specified
"""
# Determine colors - prioritize artist mood, then genre, then theme
if artist:
# Normalize artist name (remove spaces, lowercase)
artist_key = artist.lower().replace(" ", "").replace("the", "")
if artist_key in ARTIST_MOODS:
gradient_start, gradient_end, text_color = ARTIST_MOODS[artist_key]
elif theme and theme in THEME_COLORS:
gradient_start, gradient_end, text_color = THEME_COLORS[theme]
elif genre and genre in GENRE_COLORS:
gradient_start, gradient_end, text_color = GENRE_COLORS[genre]
else:
# Default to energetic theme if artist not found
gradient_start, gradient_end, text_color = THEME_COLORS["energetic"]
elif theme and theme in THEME_COLORS:
gradient_start, gradient_end, text_color = THEME_COLORS[theme]
elif genre and genre in GENRE_COLORS:
gradient_start, gradient_end, text_color = GENRE_COLORS[genre]
elif not (gradient_start and gradient_end and text_color):
raise ValueError(
"Must specify theme, genre, artist, or custom colors "
"(gradient_start, gradient_end, text_color)"
)
# Generate SVG
svg_content = self._create_svg(
title=title,
subtitle=subtitle,
gradient_start=gradient_start,
gradient_end=gradient_end,
text_color=text_color,
size=size
)
# Convert SVG to PNG
png_data = cairosvg.svg2png(
bytestring=svg_content.encode('utf-8'),
output_width=size,
output_height=size
)
# Save to file or temp file
if output_path is None:
output_path = "temp_cover.png"
with open(output_path, 'wb') as f:
f.write(png_data)
# Optimize image size
self._optimize_image(output_path, max_size_kb=256)
return output_path
def _create_svg(
self,
title: str,
subtitle: str,
gradient_start: str,
gradient_end: str,
text_color: str,
size: int = 600
) -> str:
"""
Create SVG content with gradient background and text.
Optimized for thumbnail readability with large typography at 80% width.
Automatically wraps long titles across multiple lines.
Args:
title: Main title text
subtitle: Subtitle text
gradient_start: Gradient start color (hex)
gradient_end: Gradient end color (hex)
text_color: Text color (hex)
size: Canvas size in pixels
Returns:
SVG content as string
"""
center = size // 2
text_width = size * 0.8 # 80% of cover width for text
# Calculate dynamic font sizes based on text length for optimal thumbnail readability
# Larger fonts for shorter text, scaled down for longer text
title_length = len(title)
if title_length <= 10:
title_font_size = 96 # Very large for short titles
elif title_length <= 15:
title_font_size = 84
elif title_length <= 20:
title_font_size = 72
else:
title_font_size = 60 # Still large for longer titles
subtitle_font_size = title_font_size * 0.45 # Subtitle is 45% of title size
# Word wrap title if too long (more than 20 characters)
title_lines = self._wrap_text(title, max_chars_per_line=20)
num_title_lines = len(title_lines)
# Calculate vertical positioning based on number of lines
line_height = title_font_size * 1.1 # 110% of font size for line spacing
total_title_height = line_height * num_title_lines
# Vertical spacing to prevent overlap
if subtitle:
# Start title higher to make room for subtitle
title_start_y = center - (total_title_height / 2) - (subtitle_font_size * 0.5)
subtitle_y = title_start_y + total_title_height + (subtitle_font_size * 1.2)
else:
# Center the title
title_start_y = center - (total_title_height / 2)
subtitle_y = 0
svg = f'''<svg width="{size}" height="{size}" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="grad" cx="50%" cy="50%" r="50%">
<stop offset="0%" style="stop-color:{gradient_start};stop-opacity:1" />
<stop offset="100%" style="stop-color:{gradient_end};stop-opacity:1" />
</radialGradient>
</defs>
<!-- Background gradient -->
<rect width="{size}" height="{size}" fill="url(#grad)" />
<!-- Decorative shapes (positioned to avoid text) -->
<circle cx="{size * 0.85}" cy="{size * 0.15}" r="{size * 0.12}"
fill="{text_color}" opacity="0.08" />
<circle cx="{size * 0.15}" cy="{size * 0.85}" r="{size * 0.08}"
fill="{text_color}" opacity="0.12" />
<!-- Title text with multi-line support -->
'''
# Add each line of the title
for i, line in enumerate(title_lines):
line_y = title_start_y + (i * line_height)
svg += f''' <text x="{center}" y="{line_y}"
font-family="Arial Black, Arial Bold, Arial, sans-serif"
font-size="{title_font_size}"
font-weight="900"
fill="{text_color}"
text-anchor="middle"
dominant-baseline="middle">
{self._escape_xml(line)}
</text>
'''
# Add subtitle if provided (with proper spacing)
if subtitle:
svg += f'''
<!-- Subtitle text -->
<text x="{center}" y="{subtitle_y}"
font-family="Arial, sans-serif"
font-size="{subtitle_font_size}"
font-weight="600"
fill="{text_color}"
text-anchor="middle"
opacity="0.95">
{self._escape_xml(subtitle)}
</text>
'''
svg += '</svg>'
return svg
def _wrap_text(self, text: str, max_chars_per_line: int = 20) -> List[str]:
"""
Wrap text into multiple lines, trying to break at word boundaries.
Args:
text: Text to wrap
max_chars_per_line: Maximum characters per line
Returns:
List of text lines
"""
if len(text) <= max_chars_per_line:
return [text]
words = text.split()
lines = []
current_line = []
current_length = 0
for word in words:
word_length = len(word)
# +1 for space
if current_length + word_length + (1 if current_line else 0) <= max_chars_per_line:
current_line.append(word)
current_length += word_length + (1 if len(current_line) > 1 else 0)
else:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
current_length = word_length
if current_line:
lines.append(' '.join(current_line))
return lines
def _escape_xml(self, text: str) -> str:
"""
Escape special XML characters.
Args:
text: Input text
Returns:
XML-safe text
"""
return (text
.replace('&', '&')
.replace('<', '<')
.replace('>', '>')
.replace('"', '"')
.replace("'", '''))
def _optimize_image(self, image_path: str, max_size_kb: int = 256) -> None:
"""
Optimize PNG image to meet Spotify's size requirements.
Args:
image_path: Path to PNG image
max_size_kb: Maximum file size in KB (default 256 for Spotify)
"""
max_size_bytes = max_size_kb * 1024
# Check current size
current_size = os.path.getsize(image_path)
if current_size <= max_size_bytes:
return # Already meets requirements
# Load image
img = Image.open(image_path)
# Try reducing quality
quality = 95
while quality > 20:
output = BytesIO()
img.save(output, format='PNG', optimize=True, quality=quality)
if output.tell() <= max_size_bytes:
# Save optimized image
with open(image_path, 'wb') as f:
f.write(output.getvalue())
return
quality -= 5
# If still too large, resize
scale = 0.9
while os.path.getsize(image_path) > max_size_bytes and scale > 0.5:
new_size = (int(img.width * scale), int(img.height * scale))
resized = img.resize(new_size, Image.Resampling.LANCZOS)
output = BytesIO()
resized.save(output, format='PNG', optimize=True)
if output.tell() <= max_size_bytes:
with open(image_path, 'wb') as f:
f.write(output.getvalue())
return
scale -= 0.1
def upload_cover_image(self, playlist_id: str, image_path: str) -> bool:
"""
Upload cover image to Spotify playlist.
Args:
playlist_id: Spotify playlist ID
image_path: Path to image file (JPEG or PNG)
Returns:
True if successful, False otherwise
Example:
>>> generator.upload_cover_image(
... "37i9dQZF1DXcBWIGoYBM5M",
... "my_cover.png"
... )
True
"""
try:
# Read and encode image
with open(image_path, 'rb') as f:
image_data = f.read()
# Base64 encode
encoded_image = base64.b64encode(image_data).decode('utf-8')
# Upload to Spotify
url = f"{self.base_url}/playlists/{playlist_id}/images"
headers = {
"Authorization": f"Bearer {self.access_token}",
"Content-Type": "image/jpeg"
}
response = requests.put(url, headers=headers, data=encoded_image)
if response.status_code == 202:
print(f"✓ Cover art uploaded successfully to playlist {playlist_id}")
return True
elif response.status_code == 401:
print(f"✗ Failed to upload cover art: 401 Unauthorized")
print(f" Response: {response.text}")
print("\n⚠️ MISSING SCOPE: The 'ugc-image-upload' scope is required!")
print("\nTo fix this:")
print("1. Go to https://developer.spotify.com/dashboard")
print("2. Select your app and ensure it has the 'ugc-image-upload' scope")
print("3. Re-run the OAuth flow to get a new refresh token with this scope")
print("4. Update your .env file with the new refresh token")
print("\nAlternatively, you can:")
print("- Generate cover art locally (it will save as PNG)")
print("- Manually upload it to Spotify via the web/mobile app")
return False
else:
print(f"✗ Failed to upload cover art: {response.status_code}")
print(f" Response: {response.text}")
return False
except Exception as e:
print(f"✗ Error uploading cover image: {e}")
return False
# Example usage
if __name__ == "__main__":
# Load credentials from environment
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
ACCESS_TOKEN = os.getenv("SPOTIFY_ACCESS_TOKEN")
if not all([CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN]):
print("Error: Missing Spotify credentials in environment variables")
print("Set SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, and SPOTIFY_ACCESS_TOKEN")
exit(1)
# Initialize generator
generator = CoverArtGenerator(CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN)
# Example: Generate cover art with 'summer' theme
print("Generating sample cover art with 'summer' theme...")
png_path = generator.generate_cover_art(
title="Summer Vibes",
subtitle="Feel Good Hits",
theme="summer",
output_path="sample_cover.png"
)
print(f"✓ Cover art saved to: {png_path}")
# To upload to a playlist:
# generator.create_and_upload_cover(
# playlist_id="your_playlist_id",
# title="My Playlist",
# subtitle="2024",
# theme="summer"
# )
"""
Export Spotify data as JSON for use in applications without API calls.
This script fetches data from Spotify API and saves it as JSON files that can be
imported directly into React/web applications, avoiding runtime API calls.
"""
import json
import os
from pathlib import Path
from typing import Dict, List, Optional
from spotify_client import create_client_from_env, validate_credentials, get_validation_errors
class SpotifyDataExporter:
"""Export Spotify data to JSON files."""
def __init__(self, output_dir: str = "exported_data"):
"""
Initialize exporter.
Args:
output_dir: Directory to save exported JSON files
"""
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
# Initialize client
self.client = create_client_from_env()
if self.client.refresh_token:
self.client.refresh_access_token()
def export_user_profile(self) -> Dict:
"""
Export user profile data.
Returns:
User profile dictionary
"""
print("📊 Exporting user profile...")
user = self.client.get_current_user()
# Sanitize data for export
exported = {
'id': user.get('id'),
'display_name': user.get('display_name'),
'email': user.get('email'),
'country': user.get('country'),
'product': user.get('product'),
'followers': user.get('followers', {}).get('total', 0),
'images': user.get('images', []),
'external_urls': user.get('external_urls', {}),
'exported_at': self._get_timestamp()
}
self._save_json('user_profile.json', exported)
print(f" ✓ Saved to {self.output_dir}/user_profile.json")
return exported
def export_playlists(self, limit: int = 50) -> List[Dict]:
"""
Export user's playlists.
Args:
limit: Maximum number of playlists to export
Returns:
List of playlist dictionaries
"""
print(f"📊 Exporting playlists (limit: {limit})...")
playlists = self.client.get_user_playlists(limit=limit)
# Sanitize data for export
exported = []
for playlist in playlists:
exported.append({
'id': playlist.get('id'),
'name': playlist.get('name'),
'description': playlist.get('description'),
'public': playlist.get('public'),
'tracks_total': playlist.get('tracks', {}).get('total', 0),
'images': playlist.get('images', []),
'external_urls': playlist.get('external_urls', {}),
'owner': {
'id': playlist.get('owner', {}).get('id'),
'display_name': playlist.get('owner', {}).get('display_name')
}
})
self._save_json('playlists.json', exported)
print(f" ✓ Saved {len(exported)} playlists to {self.output_dir}/playlists.json")
return exported
def export_top_artists(self, time_range: str = 'medium_term', limit: int = 20) -> List[Dict]:
"""
Export user's top artists.
Args:
time_range: 'short_term', 'medium_term', or 'long_term'
limit: Maximum number of artists to export
Returns:
List of artist dictionaries
"""
print(f"📊 Exporting top artists ({time_range}, limit: {limit})...")
artists = self.client.get_user_top_artists(time_range=time_range, limit=limit)
# Sanitize data for export
exported = []
for artist in artists:
exported.append({
'id': artist.get('id'),
'name': artist.get('name'),
'genres': artist.get('genres', []),
'popularity': artist.get('popularity'),
'followers': artist.get('followers', {}).get('total', 0),
'images': artist.get('images', []),
'external_urls': artist.get('external_urls', {})
})
filename = f'top_artists_{time_range}.json'
self._save_json(filename, exported)
print(f" ✓ Saved {len(exported)} artists to {self.output_dir}/{filename}")
return exported
def export_top_tracks(self, time_range: str = 'medium_term', limit: int = 20) -> List[Dict]:
"""
Export user's top tracks.
Args:
time_range: 'short_term', 'medium_term', or 'long_term'
limit: Maximum number of tracks to export
Returns:
List of track dictionaries
"""
print(f"📊 Exporting top tracks ({time_range}, limit: {limit})...")
tracks = self.client.get_user_top_tracks(time_range=time_range, limit=limit)
# Sanitize data for export
exported = []
for track in tracks:
exported.append({
'id': track.get('id'),
'name': track.get('name'),
'artists': [{'id': a.get('id'), 'name': a.get('name')} for a in track.get('artists', [])],
'album': {
'id': track.get('album', {}).get('id'),
'name': track.get('album', {}).get('name'),
'images': track.get('album', {}).get('images', [])
},
'duration_ms': track.get('duration_ms'),
'popularity': track.get('popularity'),
'preview_url': track.get('preview_url'),
'external_urls': track.get('external_urls', {})
})
filename = f'top_tracks_{time_range}.json'
self._save_json(filename, exported)
print(f" ✓ Saved {len(exported)} tracks to {self.output_dir}/{filename}")
return exported
def export_playlist_tracks(self, playlist_id: str, playlist_name: Optional[str] = None) -> List[Dict]:
"""
Export tracks from a specific playlist.
Args:
playlist_id: Spotify playlist ID
playlist_name: Optional name for the output file
Returns:
List of track dictionaries
"""
print(f"📊 Exporting playlist tracks (ID: {playlist_id})...")
tracks = self.client.get_playlist_tracks(playlist_id)
# Sanitize data for export
exported = []
for item in tracks:
track = item.get('track', {})
if track:
exported.append({
'id': track.get('id'),
'name': track.get('name'),
'artists': [{'id': a.get('id'), 'name': a.get('name')} for a in track.get('artists', [])],
'album': {
'id': track.get('album', {}).get('id'),
'name': track.get('album', {}).get('name'),
'images': track.get('album', {}).get('images', [])
},
'duration_ms': track.get('duration_ms'),
'added_at': item.get('added_at'),
'external_urls': track.get('external_urls', {})
})
filename = f'playlist_{playlist_name or playlist_id}.json'
filename = filename.replace(' ', '_').replace('/', '_')
self._save_json(filename, exported)
print(f" ✓ Saved {len(exported)} tracks to {self.output_dir}/{filename}")
return exported
def export_all(self):
"""Export all available data."""
print("\n" + "=" * 60)
print("🎵 Spotify Data Export")
print("=" * 60 + "\n")
try:
self.export_user_profile()
self.export_playlists()
self.export_top_artists()
self.export_top_tracks()
print("\n" + "=" * 60)
print("✅ Export complete!")
print(f"📁 Files saved to: {self.output_dir.absolute()}")
print("=" * 60 + "\n")
except Exception as e:
print(f"\n❌ Export failed: {str(e)}\n")
raise
def _save_json(self, filename: str, data: any):
"""Save data as JSON file."""
filepath = self.output_dir / filename
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def _get_timestamp(self) -> str:
"""Get current timestamp."""
from datetime import datetime
return datetime.now().isoformat()
def main():
"""Main entry point."""
# Validate credentials first
validation = validate_credentials()
if not validation['all_valid']:
print("\n❌ Missing Spotify credentials!\n")
errors = get_validation_errors()
for error in errors:
print(error)
print()
return 1
# Export data
try:
exporter = SpotifyDataExporter()
exporter.export_all()
print("💡 Usage in React/Web apps:")
print(" import userData from './exported_data/user_profile.json';")
print(" import playlists from './exported_data/playlists.json';")
print()
return 0
except Exception as e:
print(f"\n❌ Error: {str(e)}\n")
return 1
if __name__ == '__main__':
exit(main())
"""
Spotify Intelligent Playlist Creator
High-level utility for creating playlists from various sources:
- Artist/band name
- Theme/mood keywords
- Lyrics-based content search
- Specific song lists
"""
from typing import List, Dict, Optional, Any
from spotify_client import SpotifyClient
class PlaylistCreator:
"""Create playlists through various methods."""
def __init__(self, client: SpotifyClient):
"""Initialize with Spotify client."""
self.client = client
self.max_tracks_per_playlist = 100
def create_from_artist(self, artist_name: str, playlist_name: str = None,
playlist_description: str = "", public: bool = True,
limit: int = 50) -> Dict[str, Any]:
"""
Create playlist from artist's top tracks.
Args:
artist_name: Name of artist or band
playlist_name: Playlist name (defaults to artist name)
playlist_description: Optional description
public: Make playlist public
limit: Number of tracks to add
Returns:
Playlist data with track count
"""
# Search for artist
artists = self.client.search_artists(query=artist_name, limit=1)
if not artists:
raise ValueError(f"Artist '{artist_name}' not found")
artist_id = artists[0]["id"]
artist_name_actual = artists[0]["name"]
# Get artist's top tracks
tracks = self.client.get_artist_top_tracks(
artist_id=artist_id,
limit=min(limit, 50)
)
track_ids = [t["id"] for t in tracks]
# Create playlist
playlist_name = playlist_name or f"{artist_name_actual} Collection"
if not playlist_description:
playlist_description = f"Curated collection of {artist_name_actual}'s top tracks"
playlist = self.client.create_playlist(
name=playlist_name,
description=playlist_description,
public=public
)
# Add tracks
if track_ids:
self.client.add_tracks_to_playlist(playlist["id"], track_ids)
return {
"playlist": playlist,
"tracks_added": len(track_ids),
"artist": artist_name_actual
}
def create_from_theme(self, theme_keywords: List[str], playlist_name: str,
playlist_description: str = "", public: bool = True,
limit: int = 100) -> Dict[str, Any]:
"""
Create playlist based on theme/mood keywords.
Args:
theme_keywords: List of theme keywords (e.g., ["chill", "indie", "2020s"])
playlist_name: Playlist name
playlist_description: Optional description
public: Make playlist public
limit: Maximum tracks to add
Returns:
Playlist data with track count and keywords used
"""
all_tracks = []
track_ids_set = set()
# Search for each keyword
for keyword in theme_keywords:
results = self.client.search_tracks(query=keyword, limit=30)
for track in results:
track_id = track["id"]
if track_id not in track_ids_set:
all_tracks.append(track)
track_ids_set.add(track_id)
# Stop if we have enough
if len(all_tracks) >= limit:
break
# Limit tracks
track_ids = [t["id"] for t in all_tracks[:limit]]
if not track_ids:
raise ValueError(f"No tracks found for theme keywords: {theme_keywords}")
# Create playlist
playlist = self.client.create_playlist(
name=playlist_name,
description=playlist_description,
public=public
)
# Add tracks in batches (Spotify limit is 100 per request)
for i in range(0, len(track_ids), 100):
batch = track_ids[i:i+100]
self.client.add_tracks_to_playlist(playlist["id"], batch)
return {
"playlist": playlist,
"tracks_added": len(track_ids),
"keywords": theme_keywords
}
def create_from_lyrics(self, lyric_keywords: List[str], playlist_name: str,
playlist_description: str = "", public: bool = True,
limit: int = 100) -> Dict[str, Any]:
"""
Create playlist based on lyrical content.
Args:
lyric_keywords: List of lyrical themes (e.g., ["love", "heartbreak", "midnight"])
playlist_name: Playlist name
playlist_description: Optional description
public: Make playlist public
limit: Maximum tracks to add
Returns:
Playlist data with track count and keywords used
"""
all_tracks = []
track_ids_set = set()
# Search for each lyric keyword
for keyword in lyric_keywords:
results = self.client.search_tracks(query=keyword, limit=30)
for track in results:
track_id = track["id"]
if track_id not in track_ids_set:
all_tracks.append(track)
track_ids_set.add(track_id)
# Stop if we have enough
if len(all_tracks) >= limit:
break
# Limit and deduplicate
track_ids = list(dict.fromkeys([t["id"] for t in all_tracks[:limit]]))
if not track_ids:
raise ValueError(f"No tracks found for lyric keywords: {lyric_keywords}")
# Create playlist
playlist = self.client.create_playlist(
name=playlist_name,
description=playlist_description,
public=public
)
# Add tracks in batches
for i in range(0, len(track_ids), 100):
batch = track_ids[i:i+100]
self.client.add_tracks_to_playlist(playlist["id"], batch)
return {
"playlist": playlist,
"tracks_added": len(track_ids),
"lyric_keywords": lyric_keywords
}
def create_from_song_list(self, song_list: List[str], playlist_name: str,
playlist_description: str = "", public: bool = True) -> Dict[str, Any]:
"""
Create playlist from specific song list.
Args:
song_list: List of song names or search queries
playlist_name: Playlist name
playlist_description: Optional description
public: Make playlist public
Returns:
Playlist data with found tracks and missing songs
"""
track_ids = []
not_found = []
# Search for each song
for song_query in song_list:
results = self.client.search_tracks(query=song_query, limit=1)
if results:
track_ids.append(results[0]["id"])
else:
not_found.append(song_query)
if not track_ids:
raise ValueError(f"No tracks found from song list")
# Create playlist
playlist = self.client.create_playlist(
name=playlist_name,
description=playlist_description,
public=public
)
# Add tracks in batches
for i in range(0, len(track_ids), 100):
batch = track_ids[i:i+100]
self.client.add_tracks_to_playlist(playlist["id"], batch)
return {
"playlist": playlist,
"tracks_added": len(track_ids),
"tracks_found": len(track_ids),
"tracks_not_found": len(not_found),
"not_found_songs": not_found if not_found else None
}
def create_from_recommendations(self, playlist_name: str,
seed_artists: List[str] = None,
seed_tracks: List[str] = None,
seed_genres: List[str] = None,
playlist_description: str = "",
public: bool = True,
limit: int = 100) -> Dict[str, Any]:
"""
Create playlist from Spotify recommendations.
Args:
playlist_name: Playlist name
seed_artists: Artist IDs (max 5)
seed_tracks: Track IDs (max 5)
seed_genres: Genres (max 5)
playlist_description: Optional description
public: Make playlist public
limit: Number of recommendations (max 100)
Returns:
Playlist data with track count
"""
# Get recommendations
recommended_tracks = self.client.get_recommendations(
seed_artists=seed_artists,
seed_tracks=seed_tracks,
seed_genres=seed_genres,
limit=min(limit, 100)
)
if not recommended_tracks:
raise ValueError("No recommendations found for provided seeds")
track_ids = [t["id"] for t in recommended_tracks]
# Create playlist
playlist = self.client.create_playlist(
name=playlist_name,
description=playlist_description,
public=public
)
# Add tracks
self.client.add_tracks_to_playlist(playlist["id"], track_ids)
return {
"playlist": playlist,
"tracks_added": len(track_ids),
"recommendation_seeds": {
"artists": seed_artists or [],
"tracks": seed_tracks or [],
"genres": seed_genres or []
}
}
def add_playlist_artwork(self, playlist_id: str, image_base64: str) -> None:
"""
Add cover image to playlist (if supported by API).
Note: Full image upload support requires additional endpoint.
This is a placeholder for future enhancement.
"""
# This would require additional implementation
pass
def get_playlist_stats(self, playlist_id: str) -> Dict[str, Any]:
"""Get statistics about a playlist."""
playlist = self.client.get_playlist(playlist_id)
tracks = self.client.get_playlist_tracks(playlist_id, limit=1)
total_tracks = playlist.get("tracks", {}).get("total", 0)
# Get all tracks to calculate duration
all_tracks = []
offset = 0
while offset < total_tracks:
batch = self.client.get_playlist_tracks(
playlist_id,
limit=50,
offset=offset
)
all_tracks.extend(batch)
offset += 50
total_duration_ms = sum(
t.get("track", {}).get("duration_ms", 0) for t in all_tracks
)
return {
"name": playlist.get("name"),
"total_tracks": total_tracks,
"total_duration_minutes": total_duration_ms // 60000,
"public": playlist.get("public"),
"collaborative": playlist.get("collaborative"),
"owner": playlist.get("owner", {}).get("display_name"),
"followers": playlist.get("followers", {}).get("total", 0)
}
"""
Spotify API Skill - Credential and API Test
Run this script to verify your Spotify credentials are correctly configured
and the API is accessible.
"""
import sys
from pathlib import Path
# Add current directory (scripts) to path
sys.path.insert(0, str(Path(__file__).parent))
from spotify_client import create_client_from_env, check_network_access, NetworkAccessError, validate_credentials, get_validation_errors
def test_credentials():
"""Test if credentials are loaded and valid."""
print("=" * 60)
print("Spotify API Skill - Credential Test")
print("=" * 60)
print()
try:
# Test 0: Check network access first
print("0️⃣ Checking network access to api.spotify.com...")
try:
check_network_access()
print(" ✓ Network access OK - api.spotify.com is reachable")
except NetworkAccessError as e:
print(str(e))
return False
print()
# Test 0.5: Validate credentials before loading
print("0️⃣.5 Validating credentials...")
validation = validate_credentials()
if not validation['all_valid']:
print(" ❌ Credential validation failed!\n")
errors = get_validation_errors()
for error in errors:
print(f" {error}")
print()
return False
print(" ✓ All required credentials found")
print()
# Test 1: Load credentials
print("1️⃣ Loading credentials from .env file...")
client = create_client_from_env()
print(f" ✓ Client ID: {client.client_id[:15]}...")
print(f" ✓ Client Secret: {client.client_secret[:15]}...")
print(f" ✓ Redirect URI: {client.redirect_uri}")
if not client.refresh_token:
print("\n⚠️ WARNING: No refresh token found!")
print(" You need to complete OAuth flow to get a refresh token.")
print(" See references/authentication_guide.md for instructions.")
return False
print(f" ✓ Refresh Token: {client.refresh_token[:25]}...")
print()
# Test 2: Refresh access token
print("2️⃣ Refreshing access token...")
token_data = client.refresh_access_token()
print(f" ✓ Access token obtained: {client.access_token[:25]}...")
print(f" ✓ Token expires in: {token_data.get('expires_in', 'unknown')} seconds")
print()
# Test 3: Make API call
print("3️⃣ Testing API access...")
user = client.get_current_user()
print(f" ✓ Successfully connected to Spotify API!")
print(f" ✓ User: {user.get('display_name', 'N/A')}")
print(f" ✓ User ID: {user.get('id', 'N/A')}")
print(f" ✓ Email: {user.get('email', 'N/A')}")
print(f" ✓ Country: {user.get('country', 'N/A')}")
print(f" ✓ Product: {user.get('product', 'N/A')}")
print()
# Test 4: Quick functionality check
print("4️⃣ Testing basic functionality...")
playlists = client.get_user_playlists(limit=3)
print(f" ✓ Found {len(playlists)} playlists")
artists = client.search_artists(query="The Beatles", limit=1)
print(f" ✓ Search working (found: {artists[0]['name'] if artists else 'none'})")
print()
print("=" * 60)
print("✅ ALL TESTS PASSED!")
print("=" * 60)
print()
print("Your Spotify API skill is configured correctly and ready to use!")
print()
return True
except ValueError as e:
print(f"\n❌ Configuration Error: {e}")
print()
print("Please check your .env file and ensure all required credentials are set:")
print(" - SPOTIFY_CLIENT_ID")
print(" - SPOTIFY_CLIENT_SECRET")
print(" - SPOTIFY_REFRESH_TOKEN")
print()
return False
except Exception as e:
print(f"\n❌ Error: {e}")
print()
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = test_credentials()
sys.exit(0 if success else 1)