← Back to list

Building a Spotify Song Recommender System with Python and Streamlit

Ever wondered how Spotify seems to know what songs you’ll like next? I wanted to explore the mechanics behind recommender systems, and I…

Ahmed Faraz Shaikh · 2025-08-23 19:21 · 0 claps · 5.0 min read
#recommender-systems #spotify #artist #instance-based-learning #machine-learning
Open on Medium ↗
Wiki topics: ML · Machine Learning GEN · Genomics & Sequencing EDU · Education & Learning 🎵 · Music & Audio

Building a Spotify Song Recommender System with Python and Streamlit

Ever wondered how Spotify seems to know what songs you’ll like next? I wanted to explore the mechanics behind recommender systems, and I thought a fun way to do it would be to build a song recommender that finds similar tracks based on artist, genre, and subgenre using cosine similarity. In this blog, I’ll take you step by step through how I built my project, from preparing the data to creating a Streamlit app where you can see recommendations with covers and 30-seconds previews.

I built this project as a hands-on way to understand recommender systems, and you can check out the full code on Github: https://github.com/ahmedfarazsyk/spotify_song_recommender

Here we will use instance-based learning approach instead of the traditional model-based learning.

Instance-Based Learning

The algorithm doesn’t build a general model or coefficients. It stores all training data (song feature vectors) and computes similarity on the fly. When you query with a seed song, it looks up its neighbors in the dataset using cosine similarity and returns the top matches. This is exactly what this recommender does: it uses the similarity matrix (precomputed from instances) to find recommendations.

Instance-based learning

Instance-based learning

Getting the Data

I started with a dataset of 18,454 songs available on Kaggle: https://www.kaggle.com/datasets/sujaykapadnis/spotify-songs

It includes metadata like:

track_id, track_name, track_artist, lyrics, track_popularity, track_album_id, track_album_name, track_album_release_date, playlist_name, playlist_id, playlist_genre, playlist_subgenre, danceability, energy, key, loudness, mode, speechiness, acousticness, instrumentalness, liveness, valence, tempo, duration_ms, language

The goal was to represent each song in a way that allows comparison with other songs. Features like artist, genre, and subgenre are categorical, so I needed a method that could encode them numerically to compute similarity.

Taking a Quick Look at the Data

Before jumping into modeling, I explored the dataset:

import pandas as pd
df = pd.read_csv("spotify_songs.csv")
print(df.head())
print(df.info())

This gave me a clear view of the columns I’d use and any missing values that needed handling. Initially, I thought that lyrics would be a better option to find the similar songs. But after building a similarity matrix, it gave me the most irrelevant songs. They were similar based on the words repeated in lyrics but they have completely different genre and artist. When we search for some song on Spotify we are often recommended with songs that have same genre, sub genre or artist. So this time I opted for Playlist Genre, Playlist Sub Genre and Track Artist as my key attributes.

Data Transformation

Categorical text features need to be converted into numeric vectors. But before that I needed to clean the data a little bit. There are many artists whose first name is similar, there are 5946 unique artists in the data, but 4412 unique artists by their first name. Eg. The Weeknd, The Cranberries, The Game, The Who, The Rolling Stones etc. So if we just build a similarity matrix without transformation that would treat different artists similarly and first name and last name of a same artist differently. Same goes for Genre and Sub Genre.

The solution is to remove the spaces between the words, so each name is treated differently:

song_df["track_artist"] =  song_df["track_artist"].apply(lambda x: x.replace(" ", "" ))
song_df["playlist_genre"] = song_df["playlist_genre"].apply(lambda x: x.replace(" ", ""))
song_df["playlist_subgenre"] = song_df["playlist_subgenre"].apply(lambda x: x.replace(" ", ""))

I combined artist, genre, and subgenre then used CountVectorizer to form a single feature matrix with 6000 most frequent words.

After transformation, each song was represented as a vector in a high-dimensional space. This makes it possible to compute cosine similarity between songs.

Building the Cosine Similarity Matrix

Cosine similarity measures how close two vectors are in this feature space. Mathematically:

cosine similarity = A⋅B / ∣∣A∣∣ ∣∣B∣∣

I used scikit-learn to compute a large similarity matrix (size 18,454 × 18,454):

from sklearn.metrics.pairwise import cosine_similarity

similarity = cosine_similarity(vectors)

Using Spotify API to Extract Relevant Data

While the similarity matrix finds the closest songs, I wanted to show album covers and 30-second previews in my app. For this, I used the Spotify Web API:

  1. Setup: Create a Spotify Developer App to get CLIENT_ID and CLIENT_SECRET.
  2. Fetch track metadata (covers and preview URLs):
import requests
import base64
import os
def get_token():
    auth_string = client_id + ":" + client_secret
    auth_bytes = auth_string.encode("utf-8")
    auth_base64 = str(base64.b64encode(auth_bytes), "utf-8")

    url = "https://accounts.spotify.com/api/token"

    headers = {
        "Authorization": "Basic " + auth_base64,
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {"grant_type":"client_credentials"}

    result = post(url, headers = headers, data = data)
    json_result = json.loads(result.content)
    token = json_result["access_token"]
    return token

This is how we extract the cover of a particular track:

def get_auth_header(token):
    return {"Authorization": "Bearer " + token}

def get_cover(token, track_id):
    url = f"https://api.spotify.com/v1/tracks/{track_id}"
    headers = get_auth_header(token)
    result = get(url, headers = headers)
    json_result = json.loads(result.content)["album"]["images"][0]["url"]
    return json_result

After November 27, 2024, Spotify deprecated its 30-seconds preview url. But I found an unofficial hack. Just hit the preview url directly.

def get_track_preview_embed(track_id: str):
    """
    Scrapes Spotify embed page for a given track_id and extracts the audio preview URL.
    """
    url = f"https://open.spotify.com/embed/track/{track_id}"
    headers = {
        "User-Agent": "Mozilla/5.0"  # pretend to be a browser
    }

    response = requests.get(url, headers=headers)
    if response.status_code != 200:
        raise Exception(f"Failed to fetch embed page: {response.status_code}")

    html = response.text

    # Look for JSON containing "audioPreview"
    match = re.search(r'"audioPreview"\s*:\s*\{.*?\}', html)
    if not match:
        return None

    audio_preview_json = match.group(0)

    # Convert the matched snippet into a dict
    try:
        # Ensure it's valid JSON
        audio_preview_json = "{" + audio_preview_json + "}"
        data = json.loads(audio_preview_json)
        return data["audioPreview"]["url"]
    except Exception as e:
        print("Error parsing preview:", e)
        return None

Building the Streamlit App

Finally, I created a Streamlit web app that allows users to type a song, get recommendations, and see each song’s name, cover, and preview:

import streamlit as st

st.title("Song Recommender System")

select_song_name = st.selectbox("Which song do you want to select?",
                                songs["track_name"].values)

if st.button("Recommend"):
    names, covers, previews = recommend(select_song_name)

    st.write(song['name'])
    st.image(song['cover'])
    st.audio(song['preview'])

This provides an interactive interface for exploring similar tracks in real time.

Results

Here’s an example of a song I tried:

  • Seed Song: “Viva La Vida”
  • Top 10 Recommendations: (covers, previews, and song names shown in the app)

Limitations

  • The similarity is based purely on artist, genre, and subgenre, not on audio features or listener behavior.
  • The dataset is static; new songs won’t be included unless the matrix is recomputed.
  • Predictions are slower since the recommendations are searched around a matrix of 18454 x 18454.

Future Improvements

  • Enhance similarity by including audio embeddings (loudness, speechiness, acousticness, instrumentalness etc).
  • Dockerize the app for easy deployment.
  • Deploy to the cloud so users can try it without local setup.

Conclusion

This project shows how instance-based learning can power a simple yet effective recommender. By using cosine similarity, the system makes recommendations that are easy to understand and directly tied to the features chosen.

Of course, this approach has limits. It doesn’t scale well with huge datasets, and the quality of recommendations depends entirely on the features we feed into it. Still, it’s a solid reminder that even without complex models, good results are possible with the right representation of data.

If you’d like to explore the implementation or try it out yourself, the complete project is available here: https://github.com/ahmedfarazsyk/spotify_song_recommender


메타데이터
post_id
6e830f84ae4e
slug
building-a-spotify-song-recommender-system-with-python-and-streamlit-6e830f84ae4e
url
https://medium.com/@shaikhahmedfaraz64/building-a-spotify-song-recommender-system-with-python-and-streamlit-6e830f84ae4e
canonical_url
https://medium.com/@shaikhahmedfaraz64/building-a-spotify-song-recommender-system-with-python-and-streamlit-6e830f84ae4e
author_url
https://medium.com/@shaikhahmedfaraz64
status
ok
fetched_at
2026-06-09 14:34:10