Tools: Build a Spotify Music Time Machine with Python and OAuth 2.0

Tools: Build a Spotify Music Time Machine with Python and OAuth 2.0

Source: Dev.to

Step 1: Get Spotify Developer Credentials ## Step 2: Install Dependencies ## Step 3: Store Credentials Securely ## Step 4: Create Your First Playlist ## What Just Happened: OAuth 2.0 in 60 Seconds ## Understanding Scopes: What Permissions Mean ## Going Further: Time Ranges and Forgotten Gems ## Going Further: Audio Features and Mood Playlists ## Where to Go From Here What were you listening to this week last year? What about three years ago? If you're like most people, you have no idea. Those songs are gone -- not from Spotify's servers, but from your memory. The playlists you obsessed over in 2022 sit forgotten while your current favorites dominate your queue. Spotify gives you short-term recaps and recommendations, but it's surprisingly hard to revisit the music that defined earlier chapters of your life. You can remember an era, but not the exact tracks that were on repeat. In this tutorial, you'll build a tool that fixes that. You'll authenticate with Spotify using OAuth 2.0, fetch your personal listening data, and create playlists programmatically -- all in Python. By the end, you'll have: Prerequisites: Basic Python knowledge (variables, functions, lists). A Spotify account (free or premium). That's it. Every application that uses Spotify's API needs credentials: a Client ID and Client Secret. These identify your application to Spotify and enable OAuth authentication. 1. Go to the Spotify Developer Dashboard Visit https://developer.spotify.com/dashboard and log in with your Spotify account. Click "Create app" and fill in the form: 3. Copy Your Credentials After creating the app, click "Settings." You'll see your Client ID displayed. Click "View client secret" to reveal your Client Secret. Copy both -- you'll need them in a moment. Why localhost:8888/callback? After you authorize the application, Spotify redirects your browser to this URI with an authorization code. Spotipy (the library we'll use) opens a temporary local server on port 8888 to catch the redirect and extract the code. This is standard OAuth -- the redirect URI doesn't need to be a public server. You need two Python packages: spotipy (a Spotify API wrapper) and python-dotenv (for environment variable management). Spotipy handles the OAuth token exchange, token refresh, and API request formatting behind the scenes. You could build all of this with requests and raw HTTP calls, but Spotipy saves you from writing hundreds of lines of boilerplate for a solved problem. Create a .env file in your project directory. This file stores secrets and should never be committed to version control. Replace the placeholder values with your actual credentials from Step 1. Now add .env to your .gitignore: The .cache file appears when you run the OAuth flow -- it stores your access token locally. Keep that out of version control too. This is the moment of truth. This script authenticates with Spotify, fetches your top tracks, and creates an actual playlist in your account. Save this as quick_start.py and run it: What happens next: Your browser opens to Spotify's authorization page. You'll see the permissions your app is requesting (read top tracks, create playlists). Click "Agree." Your browser redirects to localhost:8888/callback, Spotipy catches the authorization code, exchanges it for an access token, and the script continues. Check your Spotify app. The playlist is there. You just integrated with a real-world OAuth API, fetched personalized data, and created content in your account -- all in about 30 lines of Python. When you ran that script, Spotipy handled a complete OAuth 2.0 Authorization Code flow: That's the same OAuth dance that powers "Sign in with Google," GitHub integrations, and every third-party app that connects to your accounts. The pattern is identical everywhere -- only the endpoints change. Look at the scope variable in the script: Each scope grants permission for specific operations: This is the principle of least privilege: request only the permissions you need. Spotify users see what you're asking for during the consent screen. If you request user-library-modify (which can delete saved songs) but never use it, that's a red flag. Here's where it gets interesting. Spotify's current_user_top_tracks() method accepts a time_range parameter: By comparing these time ranges, you can discover songs you've forgotten about: That's Python set subtraction doing something genuinely useful. Your long-term favorites represent songs you listened to repeatedly over years. Your short-term list is what you're playing now. The difference? Songs you provably loved but fell off your rotation. You could extend this into a full "Forgotten Gems" playlist creator: Spotify analyses every track in their catalogue and assigns numerical scores for audio characteristics. You can access these through the API: The key features, all scored 0.0 to 1.0 (except tempo, which is BPM): Combine these into "mood profiles" and you can generate playlists algorithmically: You could define profiles for focus music (low energy, high instrumentalness), chill playlists (low energy, moderate valence, acoustic), party playlists (high energy, high danceability), and anything else you can express as audio feature criteria. In this tutorial, you built a working Spotify OAuth integration, created playlists programmatically, and explored time ranges and audio features. That's a solid foundation -- but there's a lot more you could build on top of it. Each of these builds on the same pattern: fetch from Spotify's API, store or compare with historical data, generate something useful. This tutorial is adapted from Chapter 16 of Mastering APIs With Python. If you want to go deeper -- SQLite database design, error handling with retry logic, automated testing with mocks, and 29 more chapters covering everything from your first API call to deploying on AWS -- check it out. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: pip install spotipy python-dotenv Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pip install spotipy python-dotenv COMMAND_BLOCK: pip install spotipy python-dotenv CODE_BLOCK: SPOTIPY_CLIENT_ID=your_client_id_here SPOTIPY_CLIENT_SECRET=your_client_secret_here SPOTIPY_REDIRECT_URI=http://localhost:8888/callback Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: SPOTIPY_CLIENT_ID=your_client_id_here SPOTIPY_CLIENT_SECRET=your_client_secret_here SPOTIPY_REDIRECT_URI=http://localhost:8888/callback CODE_BLOCK: SPOTIPY_CLIENT_ID=your_client_id_here SPOTIPY_CLIENT_SECRET=your_client_secret_here SPOTIPY_REDIRECT_URI=http://localhost:8888/callback CODE_BLOCK: .env __pycache__/ *.pyc .cache *.db Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: .env __pycache__/ *.pyc .cache *.db CODE_BLOCK: .env __pycache__/ *.pyc .cache *.db COMMAND_BLOCK: """ Quick Start: Create your first Spotify playlist Demonstrates OAuth 2.0 authentication and basic API usage """ import os from dotenv import load_dotenv import spotipy from spotipy.oauth2 import SpotifyOAuth # Load credentials from .env file load_dotenv() # Define the permissions we need scope = "user-top-read playlist-modify-public playlist-modify-private" # Create Spotify client with OAuth sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) # Get current user info user = sp.current_user() print(f"Authenticated as: {user['display_name']}") # Fetch top tracks from last 4 weeks print("\nFetching your top tracks...") top_tracks = sp.current_user_top_tracks(limit=20, time_range='short_term') # Extract track URIs (Spotify's unique identifiers) track_uris = [track['uri'] for track in top_tracks['items']] track_names = [track['name'] for track in top_tracks['items']] # Create a new playlist print("\nCreating playlist...") playlist = sp.user_playlist_create( user=user['id'], name="My Top Tracks - Quick Start", public=False, description="Created by Music Time Machine - My current favorites" ) # Add tracks to the playlist sp.playlist_add_items(playlist['id'], track_uris) print(f"\nSuccess! Created playlist with {len(track_uris)} tracks") print(f"Playlist URL: {playlist['external_urls']['spotify']}") print("\nYour top tracks right now:") for i, name in enumerate(track_names, 1): print(f" {i}. {name}") Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: """ Quick Start: Create your first Spotify playlist Demonstrates OAuth 2.0 authentication and basic API usage """ import os from dotenv import load_dotenv import spotipy from spotipy.oauth2 import SpotifyOAuth # Load credentials from .env file load_dotenv() # Define the permissions we need scope = "user-top-read playlist-modify-public playlist-modify-private" # Create Spotify client with OAuth sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) # Get current user info user = sp.current_user() print(f"Authenticated as: {user['display_name']}") # Fetch top tracks from last 4 weeks print("\nFetching your top tracks...") top_tracks = sp.current_user_top_tracks(limit=20, time_range='short_term') # Extract track URIs (Spotify's unique identifiers) track_uris = [track['uri'] for track in top_tracks['items']] track_names = [track['name'] for track in top_tracks['items']] # Create a new playlist print("\nCreating playlist...") playlist = sp.user_playlist_create( user=user['id'], name="My Top Tracks - Quick Start", public=False, description="Created by Music Time Machine - My current favorites" ) # Add tracks to the playlist sp.playlist_add_items(playlist['id'], track_uris) print(f"\nSuccess! Created playlist with {len(track_uris)} tracks") print(f"Playlist URL: {playlist['external_urls']['spotify']}") print("\nYour top tracks right now:") for i, name in enumerate(track_names, 1): print(f" {i}. {name}") COMMAND_BLOCK: """ Quick Start: Create your first Spotify playlist Demonstrates OAuth 2.0 authentication and basic API usage """ import os from dotenv import load_dotenv import spotipy from spotipy.oauth2 import SpotifyOAuth # Load credentials from .env file load_dotenv() # Define the permissions we need scope = "user-top-read playlist-modify-public playlist-modify-private" # Create Spotify client with OAuth sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope)) # Get current user info user = sp.current_user() print(f"Authenticated as: {user['display_name']}") # Fetch top tracks from last 4 weeks print("\nFetching your top tracks...") top_tracks = sp.current_user_top_tracks(limit=20, time_range='short_term') # Extract track URIs (Spotify's unique identifiers) track_uris = [track['uri'] for track in top_tracks['items']] track_names = [track['name'] for track in top_tracks['items']] # Create a new playlist print("\nCreating playlist...") playlist = sp.user_playlist_create( user=user['id'], name="My Top Tracks - Quick Start", public=False, description="Created by Music Time Machine - My current favorites" ) # Add tracks to the playlist sp.playlist_add_items(playlist['id'], track_uris) print(f"\nSuccess! Created playlist with {len(track_uris)} tracks") print(f"Playlist URL: {playlist['external_urls']['spotify']}") print("\nYour top tracks right now:") for i, name in enumerate(track_names, 1): print(f" {i}. {name}") CODE_BLOCK: python quick_start.py Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: python quick_start.py CODE_BLOCK: python quick_start.py CODE_BLOCK: scope = "user-top-read playlist-modify-public playlist-modify-private" Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: scope = "user-top-read playlist-modify-public playlist-modify-private" CODE_BLOCK: scope = "user-top-read playlist-modify-public playlist-modify-private" COMMAND_BLOCK: # Fetch top tracks for each time range short_term = sp.current_user_top_tracks(limit=50, time_range='short_term') long_term = sp.current_user_top_tracks(limit=50, time_range='long_term') # Extract track IDs into sets short_ids = {track['id'] for track in short_term['items']} long_ids = {track['id'] for track in long_term['items']} # Forgotten gems: songs in your long-term favorites # that you haven't listened to recently forgotten = long_ids - short_ids print(f"You loved {len(forgotten)} tracks long-term but haven't heard them recently") Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Fetch top tracks for each time range short_term = sp.current_user_top_tracks(limit=50, time_range='short_term') long_term = sp.current_user_top_tracks(limit=50, time_range='long_term') # Extract track IDs into sets short_ids = {track['id'] for track in short_term['items']} long_ids = {track['id'] for track in long_term['items']} # Forgotten gems: songs in your long-term favorites # that you haven't listened to recently forgotten = long_ids - short_ids print(f"You loved {len(forgotten)} tracks long-term but haven't heard them recently") COMMAND_BLOCK: # Fetch top tracks for each time range short_term = sp.current_user_top_tracks(limit=50, time_range='short_term') long_term = sp.current_user_top_tracks(limit=50, time_range='long_term') # Extract track IDs into sets short_ids = {track['id'] for track in short_term['items']} long_ids = {track['id'] for track in long_term['items']} # Forgotten gems: songs in your long-term favorites # that you haven't listened to recently forgotten = long_ids - short_ids print(f"You loved {len(forgotten)} tracks long-term but haven't heard them recently") COMMAND_BLOCK: # Get the full track details for forgotten gems forgotten_tracks = [ track for track in long_term['items'] if track['id'] in forgotten ] # Create the playlist user = sp.current_user() playlist = sp.user_playlist_create( user=user['id'], name="Forgotten Gems", public=False, description="Songs I loved but forgot about - rediscovered by Music Time Machine" ) track_uris = [track['uri'] for track in forgotten_tracks] sp.playlist_add_items(playlist['id'], track_uris) print(f"\nCreated 'Forgotten Gems' with {len(forgotten_tracks)} tracks") for track in forgotten_tracks: print(f" - {track['name']} - {track['artists'][0]['name']}") Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Get the full track details for forgotten gems forgotten_tracks = [ track for track in long_term['items'] if track['id'] in forgotten ] # Create the playlist user = sp.current_user() playlist = sp.user_playlist_create( user=user['id'], name="Forgotten Gems", public=False, description="Songs I loved but forgot about - rediscovered by Music Time Machine" ) track_uris = [track['uri'] for track in forgotten_tracks] sp.playlist_add_items(playlist['id'], track_uris) print(f"\nCreated 'Forgotten Gems' with {len(forgotten_tracks)} tracks") for track in forgotten_tracks: print(f" - {track['name']} - {track['artists'][0]['name']}") COMMAND_BLOCK: # Get the full track details for forgotten gems forgotten_tracks = [ track for track in long_term['items'] if track['id'] in forgotten ] # Create the playlist user = sp.current_user() playlist = sp.user_playlist_create( user=user['id'], name="Forgotten Gems", public=False, description="Songs I loved but forgot about - rediscovered by Music Time Machine" ) track_uris = [track['uri'] for track in forgotten_tracks] sp.playlist_add_items(playlist['id'], track_uris) print(f"\nCreated 'Forgotten Gems' with {len(forgotten_tracks)} tracks") for track in forgotten_tracks: print(f" - {track['name']} - {track['artists'][0]['name']}") COMMAND_BLOCK: # Fetch audio features for your top tracks tracks = sp.current_user_top_tracks(limit=50)['items'] track_ids = [track['id'] for track in tracks] features_list = sp.audio_features(track_ids) # Look at what Spotify knows about a track for track, features in zip(tracks[:3], features_list[:3]): if features: print(f"\n{track['name']} - {track['artists'][0]['name']}") print(f" Energy: {features['energy']:.2f}") print(f" Valence: {features['valence']:.2f} (happiness)") print(f" Danceability: {features['danceability']:.2f}") print(f" Tempo: {features['tempo']:.0f} BPM") Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # Fetch audio features for your top tracks tracks = sp.current_user_top_tracks(limit=50)['items'] track_ids = [track['id'] for track in tracks] features_list = sp.audio_features(track_ids) # Look at what Spotify knows about a track for track, features in zip(tracks[:3], features_list[:3]): if features: print(f"\n{track['name']} - {track['artists'][0]['name']}") print(f" Energy: {features['energy']:.2f}") print(f" Valence: {features['valence']:.2f} (happiness)") print(f" Danceability: {features['danceability']:.2f}") print(f" Tempo: {features['tempo']:.0f} BPM") COMMAND_BLOCK: # Fetch audio features for your top tracks tracks = sp.current_user_top_tracks(limit=50)['items'] track_ids = [track['id'] for track in tracks] features_list = sp.audio_features(track_ids) # Look at what Spotify knows about a track for track, features in zip(tracks[:3], features_list[:3]): if features: print(f"\n{track['name']} - {track['artists'][0]['name']}") print(f" Energy: {features['energy']:.2f}") print(f" Valence: {features['valence']:.2f} (happiness)") print(f" Danceability: {features['danceability']:.2f}") print(f" Tempo: {features['tempo']:.0f} BPM") COMMAND_BLOCK: def matches_workout_profile(features): """Check if a track suits a workout playlist""" return ( features['energy'] > 0.75 and features['valence'] > 0.50 and features['tempo'] > 140 and features['danceability'] > 0.6 ) # Filter your top tracks for workout-suitable songs workout_tracks = [ tracks[i] for i in range(len(tracks)) if features_list[i] and matches_workout_profile(features_list[i]) ] print(f"Found {len(workout_tracks)} workout tracks from your top 50") Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: def matches_workout_profile(features): """Check if a track suits a workout playlist""" return ( features['energy'] > 0.75 and features['valence'] > 0.50 and features['tempo'] > 140 and features['danceability'] > 0.6 ) # Filter your top tracks for workout-suitable songs workout_tracks = [ tracks[i] for i in range(len(tracks)) if features_list[i] and matches_workout_profile(features_list[i]) ] print(f"Found {len(workout_tracks)} workout tracks from your top 50") COMMAND_BLOCK: def matches_workout_profile(features): """Check if a track suits a workout playlist""" return ( features['energy'] > 0.75 and features['valence'] > 0.50 and features['tempo'] > 140 and features['danceability'] > 0.6 ) # Filter your top tracks for workout-suitable songs workout_tracks = [ tracks[i] for i in range(len(tracks)) if features_list[i] and matches_workout_profile(features_list[i]) ] print(f"Found {len(workout_tracks)} workout tracks from your top 50") - A working OAuth 2.0 integration with Spotify - A script that creates a real playlist in your Spotify account (in about 30 lines of code) - An understanding of Spotify's time ranges and audio features - The foundation for a "Forgotten Gems" feature that rediscovers songs you loved but stopped listening to - App name: "Music Time Machine" (or whatever you prefer) - App description: "Personal music analytics and playlist generator" - Redirect URI: http://localhost:8888/callback (exactly this -- it's where OAuth redirects after authorization) - APIs used: Check "Web API" - Authorization request: Spotipy opened your browser to https://accounts.spotify.com/authorize with your Client ID, requested scopes, and redirect URI. - User consent: You saw which permissions the app needs and clicked "Agree." - Authorization code: Spotify redirected to localhost:8888/callback with a temporary authorization code in the URL. - Token exchange: Spotipy sent the authorization code (plus your Client ID and Secret) to Spotify's token endpoint and received an access token and refresh token. - API calls: Every subsequent API call (current_user_top_tracks, user_playlist_create, etc.) included the access token in the Authorization: Bearer header. - Token cached: Spotipy saved the tokens in a .cache file. Next time you run the script, it reads from cache instead of making you authorize again. When the access token expires (after 1 hour), Spotipy automatically uses the refresh token to get a new one. - user-top-read -- Read your top tracks and artists. Without this, sp.current_user_top_tracks() returns a 403 Forbidden error. - playlist-modify-public -- Create and modify public playlists. - playlist-modify-private -- Create and modify private playlists. The script creates private playlists (public=False). - Energy -- Intensity and activity. AC/DC scores ~0.9, ambient music scores ~0.1. - Valence -- Musical happiness. "Happy" by Pharrell is 0.96, "Hurt" by Johnny Cash is 0.14. - Danceability -- Beat strength and regularity. Funk and disco score high, free jazz scores low. - Tempo -- Beats per minute. Useful for workout and running playlists. - Acousticness -- How acoustic vs. electronic the track sounds. - Instrumentalness -- Predicts vocal presence. High values suggest no vocals. - Monthly snapshots: Save your top 50 tracks each month into a database. After a few months, you'll have a musical diary you can query -- "What was I listening to last March?" - Database-powered Forgotten Gems: The set-subtraction approach above works with live API data. With a SQLite database accumulating monthly snapshots, you get much richer history to mine. - Evolution analytics: Track how your music taste changes over time. Calculate turnover rates, spot genre shifts, see if your summer playlists are more upbeat. - Mood playlist generator with scoring: Instead of strict pass/fail filtering, score each track against a mood profile and take the top N matches.