Mood-based playlists demonstrate algorithmic curation: scoring tracks against criteria and selecting the best matches. You define mood profiles (combinations of energy, valence, tempo, and other audio features), then query your database for tracks that match those profiles.
This feature requires audio features to be stored in your database. If you haven't fetched them yet, the code includes a helper function that identifies tracks without features and batch-fetches them from Spotify.
Step 1: Define Mood Profiles
# Mood profile definitions
MOOD_PROFILES = {
'workout': {
'description': 'High energy tracks for running or gym',
'criteria': {
'energy_min': 0.75,
'tempo_min': 140,
'valence_min': 0.50,
'danceability_min': 0.60
}
},
'focus': {
'description': 'Low energy instrumental tracks for concentration',
'criteria': {
'energy_max': 0.40,
'tempo_min': 90,
'tempo_max': 110,
'instrumentalness_min': 0.50,
'speechiness_max': 0.10
}
},
'chill': {
'description': 'Relaxed, mellow tracks',
'criteria': {
'energy_max': 0.50,
'valence_min': 0.40,
'valence_max': 0.70,
'tempo_min': 80,
'tempo_max': 110,
'acousticness_min': 0.30
}
},
'party': {
'description': 'High energy, upbeat dance tracks',
'criteria': {
'energy_min': 0.70,
'valence_min': 0.70,
'danceability_min': 0.70,
'tempo_min': 118,
'tempo_max': 135
}
},
'melancholic': {
'description': 'Sad, introspective tracks',
'criteria': {
'energy_max': 0.50,
'valence_max': 0.30,
'acousticness_min': 0.40
}
}
}
def display_available_moods():
"""Show available mood profiles"""
print("Available mood profiles:")
for mood_name, profile in MOOD_PROFILES.items():
print(f" • {mood_name}: {profile['description']}")
print()
Step 2: Query Tracks Matching Mood
def build_mood_query(criteria):
"""
Build SQL WHERE clause from mood criteria
Args:
criteria: Dictionary of min/max constraints for audio features
Returns:
Tuple of (where_clause, params)
"""
conditions = []
params = []
for key, value in criteria.items():
if key.endswith('_min'):
feature = key[:-4] # Remove '_min' suffix
conditions.append(f"af.{feature} >= ?")
params.append(value)
elif key.endswith('_max'):
feature = key[:-4] # Remove '_max' suffix
conditions.append(f"af.{feature} <= ?")
params.append(value)
where_clause = " AND ".join(conditions)
return where_clause, params
def find_tracks_by_mood(conn, mood_name, limit=25):
"""
Find tracks matching a mood profile
Args:
conn: SQLite database connection
mood_name: Name of mood profile from MOOD_PROFILES
limit: Maximum number of tracks to return
Returns:
List of (track_id, name, artist_name, album_name) tuples
"""
if mood_name not in MOOD_PROFILES:
print(f"Unknown mood: {mood_name}")
display_available_moods()
return []
profile = MOOD_PROFILES[mood_name]
where_clause, params = build_mood_query(profile['criteria'])
query = f"""
SELECT t.track_id, t.name, t.artist_name, t.album_name,
af.energy, af.valence, af.tempo
FROM tracks t
JOIN audio_features af ON t.track_id = af.track_id
WHERE {where_clause}
ORDER BY af.energy DESC, af.tempo DESC
LIMIT ?
"""
params.append(limit)
cursor = conn.execute(query, params)
return cursor.fetchall()
Step 3: Fetch Audio Features with Rate Limiting
import time
def fetch_missing_audio_features(sp, conn):
"""
Fetch audio features for tracks that don't have them yet.
Uses batching and rate limiting to stay under Spotify's limits.
Args:
sp: Authenticated Spotipy client
conn: SQLite database connection
Returns:
Number of tracks for which features were fetched
"""
# Find tracks without audio features
cursor = conn.execute("""
SELECT track_id FROM tracks
WHERE track_id NOT IN (SELECT track_id FROM audio_features)
""")
track_ids = [row[0] for row in cursor.fetchall()]
if not track_ids:
return 0
print(f"Fetching audio features for {len(track_ids)} tracks...")
# Spotify allows 100 track IDs per request
fetched_count = 0
batch_size = 100
for i in range(0, len(track_ids), batch_size):
batch = track_ids[i:i+batch_size]
try:
features_list = sp.audio_features(batch)
for features in features_list:
if features: # Some tracks might not have features
conn.execute("""
INSERT OR REPLACE INTO audio_features (
track_id, energy, valence, danceability, tempo,
acousticness, instrumentalness, speechiness,
loudness, key, mode, time_signature
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
features['id'],
features['energy'],
features['valence'],
features['danceability'],
features['tempo'],
features['acousticness'],
features['instrumentalness'],
features['speechiness'],
features['loudness'],
features['key'],
features['mode'],
features['time_signature']
))
fetched_count += 1
# Proactive rate limiting: sleep between batches
# This keeps us comfortably under Spotify's rate limits
if i + batch_size < len(track_ids):
time.sleep(0.5)
except Exception as e:
print(f"Error fetching batch {i//batch_size + 1}: {e}")
# Continue with next batch even if this one fails
continue
conn.commit()
print(f"✓ Saved audio features for {fetched_count} tracks\n")
return fetched_count
Why the 0.5 Second Delay?
Spotify's rate limits aren't publicly documented, but testing shows they allow approximately 180 requests per minute to most endpoints. That's 3 requests per second sustained.
With 500 missing tracks split into 5 batches (100 tracks each), you're making 5 requests. Without delays, all 5 requests fire in under 1 second. This works but uses up your rate limit budget quickly if you're running multiple features simultaneously.
Adding time.sleep(0.5) spreads those 5 requests across 2.5 seconds. This keeps you comfortably under the limit while only adding 2 seconds of total runtime. The trade-off is worth it: slightly slower execution prevents rate limit errors that would require 60+ second retry delays.
Professional API integration always includes proactive throttling. React to rate limits when you hit them (with retry logic), but avoid hitting them in the first place (with deliberate pauses).
Why Batch Size Matters
Each API call has overhead (TCP connection, TLS handshake, HTTP headers, network latency). Even if each call takes only 100ms, 500 calls take 50 seconds. Five batched calls take 0.5 seconds. The 500 tracks of processing time drops from 50 seconds to under 3 seconds (including throttling delays).
Spotify rate limits by requests per second, not by data transferred. Five batched requests consume far less of your rate limit budget than 500 individual requests. This matters when processing large libraries or running multiple features simultaneously.
If one batch fails (network timeout, server error), you lose at most 100 tracks worth of features. With individual requests, failures would happen more frequently (more requests = more opportunities for failure), and you'd lose features one at a time, making progress tracking difficult.
The code uses range(0, len(track_ids), 100) to process in chunks of 100, matching Spotify's documented batch limit. Professional API integration always uses batch endpoints when available and respects documented limits.
Step 4: Create Mood Playlist
def create_mood_playlist(sp, conn, mood_name, limit=25):
"""
Create a playlist based on mood profile
Args:
sp: Authenticated Spotipy client
conn: SQLite database connection
mood_name: Name of mood profile from MOOD_PROFILES
limit: Number of tracks to include
Returns:
Playlist URL if successful, None otherwise
"""
# Ensure we have audio features
fetch_missing_audio_features(sp, conn)
# Find matching tracks
tracks = find_tracks_by_mood(conn, mood_name, limit)
if not tracks:
print(f"No tracks found matching '{mood_name}' mood profile.")
print("This usually means:")
print(" 1. You don't have enough tracks in your database yet")
print(" 2. Your music taste doesn't match this mood profile")
print("Try taking more monthly snapshots or try a different mood.")
return None
# Get user info
user = sp.current_user()
# Create playlist
profile = MOOD_PROFILES[mood_name]
playlist_name = f"{mood_name.title()} - {date.today().strftime('%B %Y')}"
playlist = sp.user_playlist_create(
user=user['id'],
name=playlist_name,
public=False,
description=f"{profile['description']} - curated by Music Time Machine"
)
# Add tracks to playlist
track_uris = [f"spotify:track:{track[0]}" for track in tracks]
sp.playlist_add_items(playlist['id'], track_uris)
# Display results
print(f"\n✓ Created playlist: {playlist_name}")
print(f"✓ Added {len(tracks)} tracks matching '{mood_name}' profile")
print(f"✓ Playlist URL: {playlist['external_urls']['spotify']}\n")
print(f"Tracks in your {mood_name} playlist:")
for i, track in enumerate(tracks[:10], 1):
track_id, name, artist, album, energy, valence, tempo = track
print(f" {i}. {name} - {artist}")
print(f" Energy: {energy:.2f}, Valence: {valence:.2f}, Tempo: {tempo:.0f} BPM")
if len(tracks) > 10:
print(f" ... and {len(tracks) - 10} more")
return playlist['external_urls']['spotify']
# Usage: Generate a workout playlist
with sqlite3.connect('music_time_machine.db') as conn:
display_available_moods()
url = create_mood_playlist(sp, conn, 'workout', limit=25)
if url:
print(f"\nOpen in Spotify: {url}")
What Just Happened: Dynamic Query Building
The build_mood_query() function converts mood criteria into SQL WHERE clauses. For the workout profile (energy_min: 0.75, tempo_min: 140), it generates WHERE af.energy >= 0.75 AND af.tempo >= 140.
This design lets you add new mood profiles without writing new queries. Just define the criteria in the MOOD_PROFILES dictionary and the code handles the rest. Want a "road trip" mood? Add it with your preferred energy, tempo, and valence thresholds.
The function fetches missing audio features before querying. This means the first time you generate a mood playlist might take 30 seconds (fetching features for 500 tracks), but subsequent playlists generate instantly because the features are cached in your database.
Available mood profiles:
• workout: High energy tracks for running or gym
• focus: Low energy instrumental tracks for concentration
• chill: Relaxed, mellow tracks
• party: High energy, upbeat dance tracks
• melancholic: Sad, introspective tracks
Fetching audio features for 327 tracks...
✓ Saved audio features for 327 tracks
✓ Created playlist: Workout - December 2024
✓ Added 25 tracks matching 'workout' profile
✓ Playlist URL: https://open.spotify.com/playlist/4kL9mN3pR...
Tracks in your workout playlist:
1. Losing My Religion - R.E.M.
Energy: 0.84, Valence: 0.52, Tempo: 163 BPM
2. The Less I Know The Better - Tame Impala
Energy: 0.83, Valence: 0.61, Tempo: 117 BPM
3. Feels Like We Only Go Backwards - Tame Impala
Energy: 0.79, Valence: 0.54, Tempo: 156 BPM
... and 22 more
Open in Spotify: https://open.spotify.com/playlist/4kL9mN3pR...
Mood playlists demonstrate why separating audio features into their own table was the right design choice. You fetch features once (slow), then query them repeatedly (fast). Creating 5 different mood playlists takes seconds after the initial feature fetch.