I'm creating a Spotify recommendation system using Flask and Spotipy. I have a feature where the system can generate 20 song recommendations based on the artists that the user has inputted in an HTML form. Everything was able to run a week ago without errors. However, as I continued to work on this system these past few days, I've been getting the SpotifyException and wasn't able to get past it. I have NOT made any changes to my code since the day it was working.
What appears in my browser:
spotipy.exceptions.SpotifyException: http status: 403, code:-1 - https://api.spotify.com/v1/audio-features/?ids=1BxfuPKGuaTgP7aM0Bbdwr,2OzhQlSqBEmt7hmkYxfT6m,4q5YezDOIPcoLr8R81x9qy,3hUxzQpSfdDqwM3ZTFQY0K,4R2kfaDFhslZEMJqAFNpdd,1dGr1c8CrMLDpV6mPbImSI,1R0a2iXumgCiFb7HEZ7gUE,0V3wPSX9ygBnCm8psDIegu,1u8c2t2Cy7UBoG4ArRcF5g,0W0iAC1VGlB82PI6elxFYf: None, reason: None
I got this error in the console:
HTTP Error for GET to https://api.spotify.com/v1/audio-features/?ids=1BxfuPKGuaTgP7aM0Bbdwr,2OzhQlSqBEmt7hmkYxfT6m,4q5YezDOIPcoLr8R81x9qy,3hUxzQpSfdDqwM3ZTFQY0K,4R2kfaDFhslZEMJqAFNpdd,1dGr1c8CrMLDpV6mPbImSI,1R0a2iXumgCiFb7HEZ7gUE,0V3wPSX9ygBnCm8psDIegu,1u8c2t2Cy7UBoG4ArRcF5g,0W0iAC1VGlB82PI6elxFYf with Params: {} returned 403 due to None
I got this when I clicked on the API call link:
{
"error": {
"status": 401,
"message": "No token provided"
}
}
What's in my app.py:
app = Flask(__name__)
app.jinja_env.globals.update(zip=zip)
app.secret_key = 'secret-key'
CLIENT_ID = 'client-id'
CLIENT_SECRET = 'client-secret'
REDIRECT_URI = 'http://localhost:5000/callback'
AUTH_URL = 'https://accounts.spotify.com/authorize'
TOKEN_URL = 'https://accounts.spotify.com/api/token'
API_BASE_URL = 'https://api.spotify.com/v1/'
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials(client_id=CLIENT_ID, client_secret=CLIENT_SECRET))
...
@app.route('/login')
def login():
scope = 'user-read-private user-read-email user-library-read user-top-read'
params = {
'client_id': CLIENT_ID,
'response_type': 'code',
'scope': scope,
'redirect_uri': REDIRECT_URI,
'show_dialog': True # set to False after testing
}
auth_url = f"{AUTH_URL}?{urllib.parse.urlencode(params)}"
return redirect(auth_url)
@app.route('/callback')
def callback():
if 'error' in request.args:
return jsonify({"error": request.args['error']})
# Login successful
if 'code' in request.args:
# Build request body with data to be sent to Spotify to get access token
req_body = {
'code': request.args['code'],
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URI,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
}
response = requests.post(TOKEN_URL, data=req_body)
token_info = response.json()
session['access_token'] = token_info['access_token']
session['refresh_token'] = token_info['refresh_token']
session['expires_at'] = datetime.now().timestamp() + token_info['expires_in']
# Access token only lasts for 1 day
# Fetch user profile
user_profile = requests.get(API_BASE_URL + 'me', headers={'Authorization': f"Bearer {token_info['access_token']}"})
user_info = user_profile.json()
# Store additional user profile data in session
session['user_name'] = user_info['display_name']
session['user_id'] = user_info['id']
session['user_email'] = user_info['email']
session['user_uri'] = user_info['uri']
session['user_link'] = user_info['external_urls']['spotify']
session['user_image'] = user_info['images'][0]['url'] if user_info.get('images') else None
return redirect(url_for('home'))
@app.route('/refresh-token')
def refresh_token():
# Check if refresh token is NOT in the session, redirect to login
if 'refresh_token' not in session:
return redirect(url_for('login'))
# If access token is expired, make a request to get a fresh one
if datetime.now().timestamp() > session['expires_at']:
req_body = {
'grant_type': 'refresh_token',
'refresh_token': session['refresh_token'],
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
}
response = requests.post(TOKEN_URL, data=req_body)
new_token_info = response.json()
# Override the access token we had before
session['access_token'] = new_token_info['access_token']
session['expires_at'] = datetime.now().timestamp() + new_token_info['expires_in']
return redirect(url_for('recommend'))
This is the route that has to do with the music recommendation:
@app.route('/new_user_recommendations', methods=['POST'])
def new_user_recommendations():
# Ensure the user is logged in
if 'access_token' not in session:
return jsonify({"error": "Please log in to use this feature"}), 401
# Retrieve artist names from the form
artist_names = [request.form.get(f'artist{i}') for i in range(1, 4)]
# Key mapping for human-readable keys
key_mapping = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
# Validate and fetch artist IDs
seed_artists = []
input_audio_features = []
for artist_name in artist_names:
if not artist_name:
return jsonify({"error": f"Missing artist name for input {artist_names.index(artist_name) + 1}"}), 400
search_result = sp.search(f'artist:"{artist_name}"', type='artist', limit=1)
if search_result['artists']['items']:
artist_id = search_result['artists']['items'][0]['id']
seed_artists.append(artist_id)
# Fetch top tracks and audio features for the artist
top_tracks = sp.artist_top_tracks(artist_id)['tracks']
track_ids = [track['id'] for track in top_tracks]
audio_features = sp.audio_features(track_ids)
# Average the features for visualization
normalized_features = {
'tempo': np.mean([feat['tempo'] for feat in audio_features if feat]),
'energy': np.mean([feat['energy'] for feat in audio_features if feat]),
'danceability': np.mean([feat['danceability'] for feat in audio_features if feat]),
'valence': np.mean([feat['valence'] for feat in audio_features if feat])
}
input_audio_features.append(normalized_features)
else:
return jsonify({"error": f"Artist '{artist_name}' not found"}), 404
# Validate that we have enough artists
if len(seed_artists) < 1:
return jsonify({"error": "No valid artists found for recommendations"}), 400
# Construct the API request
headers = {
'Authorization': f"Bearer {session['access_token']}"
}
params = {
'seed_artists': ','.join(seed_artists),
'limit': 20
}
# Add dynamic target features as needed, e.g., tempo, danceability
# params['target_tempo'] = 120
url = f'https://api.spotify.com/v1/recommendations?{urllib.parse.urlencode(params)}'
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
recommendations = response.json()
# Process recommendations
recommended_tracks = []
for track in recommendations.get('tracks', []):
# Fetch the genres for each artist in the track
track_genres = []
for artist in track['artists']:
artist_id = artist['id']
artist_info = sp.artist(artist_id)
artist_genres = artist_info.get('genres', [])
track_genres.extend(artist_genres)
recommended_tracks.append({
'track_name': track['name'],
'artists': ', '.join(artist['name'] for artist in track['artists']),
'album_name': track['album']['name'],
'popularity': track['popularity'],
'preview_url': track['preview_url'],
'genres': track_genres, # Add genres to the track
'id': track.get('id')
})
# Fetch audio features for recommended tracks
recommendation_ids = [track['id'] for track in recommended_tracks if track['id']]
rec_audio_features = sp.audio_features(recommendation_ids)
recommended_audio_features = [
{
'tempo': round(feat['tempo']),
'key': key_mapping[feat['key']],
'energy': feat['energy'],
'danceability': feat['danceability'],
'valence': feat['valence']
}
for feat in rec_audio_features if feat
]
if not recommended_tracks:
return jsonify({"error": "No recommendations found. Try with different artists"}), 404
# Render the template
return render_template(
'new_user.html',
recommendations=recommended_tracks,
input_features=input_audio_features,
recommendation_features=recommended_audio_features
)
except requests.exceptions.RequestException as e:
return jsonify({"error": f"Error fetching recommendations: {str(e)}"}), 500
The traceback mentioned that the error lies here though I'm unsure about this:
new_user_recommendations
audio_features = sp.audio_features(track_ids)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Appreciate any assistance or guidance provided.
Edit: I have tried deleting the app and creating a new one on Spotify Developers, then replacing the client ID and secret, but this still doesn't work.
Spotify has made changes to their API and mostly switched off those features for public consumption: https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api.