Tutorial: Recomendador musical con Spotify - Parte 3: el aplicativo web

En esta tercera y última parte del tutorial veremos cómo implementar el aplicativo web que nos permitirá interactuar fácilmente con el sistema de recomendación implementado anteriormente.

Al final de este tutorial se encuentra el enlace para descargar el código fuente.

Así que ¡listo, comencemos!

Video

Como siempre, en el canal de YouTube se encuentra el video de este artículo:

Introducción

En la primera parte de este tutorial vimos cómo usar la API de Spotify para generar una lista de canciones más escuchadas y de canciones candidatas, mientras que en la segunda parte usamos el filtrado basado en contenido para construir una lista de reproducción con las canciones más afines a los gustos del usuario.

En esta tercera parte veremos como conectar este backend (el sistema de recomendación que acabamos de construir) con un frontend que será un aplicativo web, implementado con la librería Flask, que facilitará nuestra interacción con el sistema de recomendación.

Comencemos entonces viendo cuál les la estructura de este proyecto.

Estructura del proyecto

En las dos primeras partes de este tutorial implementamos nuestro sistema de recomendación y lo pusimos a prueba haciendo uso de Jupyter Notebook. Sin embargo, en esta fase de producción (es decir para el desarrollo del aplicativo web) debemos eliminar algunas porciones de código que resultan innecesarias y reorganizar aquellas que resultan relevantes para nuestro aplicativo.

Así que organizaremos nuestro proyecto en un nuevo directorio (spotify_recommender) que contendrá el código asociado a esta etapa de producción y que tendrá esta estructura:

|-- /spotify_recommender
    |-- app.py
    |-- /spotipy_client
        |-- __init__.py
        |-- spotipy_client.py
    |-- /templates
        |-- home.html

donde:

En primer lugar veremos cómo tomar el código implementado en las dos primeras partes de este tutorial para crear el backend de nuestra aplicación.

Implementación del backend

Recordemos que este backend se encuentra en el sub-directorio spotipy_client que a su vez contiene los archivos spotipy_client.py e __init__.py. Comencemos hablando del primero de ellos, que contiene como tal la implementación del sistema de recomendación.

Sistema de recomendación (spotipy_client.py)

Este archivo hará uso del desarrollo realizado en las dos primeras partes del tutorial, pero contendrá únicamente el código que nos permitirá conectarnos con la API de Spotify, extraer el listado de canciones más escuchadas, generar un listado de pistas candidatas y aplicar el filtrado basado en contenido para por último generar la lista de reproducción.

En primer lugar importamos todas las librerías necesarias:

import spotipy
import spotipy.util as util
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.metrics.pairwise import linear_kernel
import numpy as np

y a continuación implementamos nuestro sistema de recomendación. Para tener un código más organizado usaremos un enfoque de Programación Orientada a Objetos para implementar la clase SpotipyClient, que contendrá todos los elementos descritos anteriormente. Comenzaremos declarando esta clase y definiendo su función para la inicialización:

class SpotipyClient():
    client = None
    client_id = None
    client_secret = None
    username = None
    redirect_uri = 'http://localhost:8080'

    def __init__(self, client_id, client_secret, username, redirect_uri, scope):
        self.client_id = client_id
        self.client_secret = client_secret
        self.username = username
        self.redirect_uri = redirect_uri
        self.scope = scope

donde los atributos de esta clase son los mismos discutidos en las dos primeras partes del tutorial y requeridos para la interacción con la API de Spotify (client_id, client_secret, username, redirect_uri y scope).

A continuación definimos el método client_auth que permite obtener las credenciales de acceso a la API de Spotify desde Python:

    def client_auth(self):
        '''Autenticación API de Spotify'''
        token = util.prompt_for_user_token(self.username,self.scope,
            self.client_id,self.client_secret,self.redirect_uri)
        self.client = spotipy.Spotify(auth=token)

los siguientes métodos permiten extraer la lista de canciones más escuchadas y un listado de pistas candidatas (estas líneas de código las vimos en detalle en la parte 1 de este tutorial):

    def get_top_tracks(self):
        '''Obtener listado de pistas más escuchadas recientemente'''
        top_tracks = self.client.current_user_top_tracks(time_range='short_term', limit=20)
        return top_tracks

    def create_tracks_dataframe(self, top_tracks):
        '''Obtener "audio features" de las pistas más escuchadas por el usuario'''
        tracks = top_tracks['items']
        tracks_ids = [track['id'] for track in tracks]
        audio_features = self.client.audio_features(tracks_ids)
        top_tracks_df = pd.DataFrame(audio_features)
        top_tracks_df = top_tracks_df[["id", "acousticness", "danceability", 
            "duration_ms", "energy", "instrumentalness",  "key", "liveness", 
            "loudness", "mode", "speechiness", "tempo", "valence"]]

        return top_tracks_df

    def get_artists_ids(self, top_tracks):
        '''Obtener ids de los artistas en "top_tracks"'''
        ids_artists = []

        for item in top_tracks['items']:
            artist_id = item['artists'][0]['id']
            artist_name = item['artists'][0]['name']
            ids_artists.append(artist_id)

        # Depurar lista para evitar repeticiones
        ids_artists = list(set(ids_artists))

        return ids_artists

    def get_similar_artists_ids(self, ids_artists):
        '''Expandir el listado de "ids_artists" con artistas similares'''
        ids_similar_artists = []
        for artist_id in ids_artists:
            artists = self.client.artist_related_artists(artist_id)['artists']
            for item in artists:
                artist_id = item['id']
                artist_name = item['name']
                ids_similar_artists.append(artist_id)

        ids_artists.extend(ids_similar_artists)

        # Depurar lista para evitar repeticiones
        ids_artists = list(set(ids_artists))

        return ids_artists

    def get_new_releases_artists_ids(self, ids_artists):
        '''Expandir el listado de "ids_artists" con artistas con nuevos lanzamientos'''

        new_releases = self.client.new_releases(limit=20)['albums']
        for item in new_releases['items']:
            artist_id = item['artists'][0]['id']
            ids_artists.append(artist_id)

        # Depurar lista para evitar repeticiones
        ids_artists = list(set(ids_artists))

        return ids_artists

    def get_albums_ids(self, ids_artists):
        '''Obtener listado de albums para cada artista en "ids_artists"'''
        ids_albums = []
        for id_artist in ids_artists:
            album = self.client.artist_albums(id_artist, limit=1)['items'][0]
            ids_albums.append(album['id'])

        return ids_albums

    def get_albums_tracks(self, ids_albums):
        '''Extraer 3 tracks para cada album en "ids_albums"'''
        ids_tracks = []
        for id_album in ids_albums:
            album_tracks = self.client.album_tracks(id_album, limit=1)['items']
            for track in album_tracks:
                ids_tracks.append(track['id'])

        return ids_tracks

    def get_tracks_features(self, ids_tracks):
        '''Extraer audio features de cada track en "ids_tracks" y almacenar resultado
        en un dataframe de Pandas'''

        ntracks = len(ids_tracks)

        if ntracks > 100:
            # Crear lotes de 100 tracks (limitacion de audio_features)
            m = ntracks//100
            n = ntracks%100
            lotes = [None]*(m+1)
            for i in range(m):
                lotes[i] = ids_tracks[i*100:i*100+100]

            if n != 0:
                lotes[i+1] = ids_tracks[(i+1)*100:]
        else:
            lotes = [ids_tracks]


        # Iterar sobre "lotes" y agregar audio features
        audio_features = []
        for lote in lotes:
            features = self.client.audio_features(lote)
            audio_features.append(features)

        audio_features = [item for sublist in audio_features for item in sublist]

        # Crear dataframe
        candidates_df = pd.DataFrame(audio_features)
        candidates_df = candidates_df[["id", "acousticness", "danceability", "duration_ms",
            "energy", "instrumentalness",  "key", "liveness", "loudness", "mode", 
            "speechiness", "tempo", "valence"]]

        return candidates_df

Ahora debemos filtrar este listado de pistas candidatas usando la similitud del coseno y el filtrado basado en contenido. Así que las líneas de código requeridas para esta parte son las mismas que vimos en detalle en la parte 2 de este tutorial:

    def compute_cossim(self, top_tracks_df, candidates_df):
        '''Calcula la similitud del coseno entre cada top_track y cada pista
        candidata en candidates_df. Retorna matriz de n_top_tracks x n_candidates_df'''
        top_tracks_mtx = top_tracks_df.iloc[:,1:].values
        candidates_mtx = candidates_df.iloc[:,1:].values

        # Estandarizar cada columna de features: mu = 0, sigma = 1
        scaler = StandardScaler()
        top_tracks_scaled = scaler.fit_transform(top_tracks_mtx)
        can_scaled = scaler.fit_transform(candidates_mtx)

        # Normalizar cada vector de características (magnitud resultante = 1)
        top_tracks_norm = np.sqrt((top_tracks_scaled*top_tracks_scaled).sum(axis=1))
        can_norm = np.sqrt((can_scaled*can_scaled).sum(axis=1))

        n_top_tracks = top_tracks_scaled.shape[0]
        n_candidates = can_scaled.shape[0]
        top_tracks = top_tracks_scaled/top_tracks_norm.reshape(n_top_tracks,1)
        candidates = can_scaled/can_norm.reshape(n_candidates,1)

        # Calcular similitudes del coseno
        cos_sim = linear_kernel(top_tracks,candidates)

        return cos_sim

    def content_based_filtering(self, pos, cos_sim, ncands, umbral = 0.8):
        '''Dada una pista de top_tracks (pos = 0, 1, ...) extraer "ncands" candidatos,
        usando "cos_sim" y siempre y cuando superen un umbral de similitud'''

        # Obtener todas las pistas candidatas por encima del umbral
        idx = np.where(cos_sim[pos,:]>=umbral)[0] # ejm. idx: [27, 82, 135]

        # Y organizarlas de forma descendente (por similitudes de mayor a menor)
        idx = idx[np.argsort(cos_sim[pos,idx])[::-1]]

        # Si hay más de "ncands", retornar únicamente un total de "ncands"
        if len(idx) >= ncands:
            cands = idx[0:ncands]
        else:
            cands = idx

        return cands

Finalmente implementaremos el método create_recommended_playlist que hace un llamado a todos los métodos anteriores para al final generar la playlist en la aplicación de Spotify:

    def create_recommended_playlist(self):
        '''Crear la lista de recomendaciones en Spotify. Ejecuta todos los métodos
        anteriores'''

        # Autenticar
        self.client_auth()

        # Obtener candidatos y compararlos (distancias coseno) con las pistas
        # del playlist original
        top_tracks = self.get_top_tracks()
        top_tracks_df = self.create_tracks_dataframe(top_tracks)
        ids_artists = self.get_artists_ids(top_tracks)
        ids_artists = self.get_similar_artists_ids(ids_artists)
        ids_artists = self.get_new_releases_artists_ids(ids_artists)
        ids_albums = self.get_albums_ids(ids_artists)
        ids_tracks = self.get_albums_tracks(ids_albums)
        candidates_df = self.get_tracks_features(ids_tracks)
        cos_sim = self.compute_cossim(top_tracks_df, candidates_df)

        # Crear listado de ids con las recomendaciones
        ids_top_tracks = []
        ids_playlist = []

        for i in range(top_tracks_df.shape[0]):
            ids_top_tracks.append(top_tracks_df['id'][i])

            # Obtener listado de candidatos (5) para esta pista
            cands = self.content_based_filtering(i, cos_sim, 5, umbral=0.7)

            # Si hay pistas relacionadas obtener los ids correspondientes
            if len(cands)==0:
                continue
            else:
                for j in cands:
                    id_cand = candidates_df['id'][j]
                    ids_playlist.append(id_cand)

        # Eliminar candidatos que ya están en top-tracks
        ids_playlist_dep = [x for x in ids_playlist if x not in ids_top_tracks]

        # Y eliminar posibles repeticiones
        ids_playlist_dep = list(set(ids_playlist_dep))

        # Crear la playlist en spotify!!!
        pl = self.client.user_playlist_create(user = self.username,
            name = 'Spotipy Recommender Playlist',
            description = 'Playlist creada con el sistema de recomendación')
        self.client.playlist_add_items(pl['id'],ids_playlist_dep)

Y listo, ya tenemos implementado el backend que es simplemente una versión organizada del código visto en las dos primeras partes de este tutorial.

El archivo __init__.py

Este archivo es requerido por Python para posteriormente importar la clase que acabamos de definir en spotipy_client.py. Esto será necesario para dar una mejor estructura a nuestra aplicación y para poder acceder a la clase recién creada desde el archivo principal de nuestro aplicativo (app.py).

Este archivo contiene tan sólo una línea de código en donde especificaremos la clase que importaremos desde spotipy_client.py:

from .spotipy_client import SpotipyClient

Y con esto ya tenemos implementado nuestro backend, la parte más compleja de nuestro aplicativo.

Nos resta únicamente el frontend, la parte más sencilla de esta etapa de producción y de la que hablaremos en detalle a continuación.

Implementación del frontend

Este frontend tiene dos elementos: el archivo home.html (ubicado en el sub-directorio templates) y la aplicación en Flask (el archivo app.py ubicado en la raíz de nuestro proyecto). El primero definirá la apariencia de nuestra página web, mientras que el segundo conectará el backend con el archivo HTML para darle funcionalidad al aplicativo.

Comencemos entonces hablando del archivo home.html.

Apariencia del aplicativo: el archivo home.html

Este archivo será como tal la página web que veremos en nuestro navegador y que nos permitirá interactuar de forma sencilla con el sistema de recomendación.

Esta página web será simplemente un archivo con extensión HTML y que tendrá una apariencia similar a esta:

La apariencia que tendrá nuestro aplicativo web (página HTML)
La apariencia que tendrá nuestro aplicativo web (página HTML)

Así que esta página contendrá:

El código para implementar este archivo HTML es el siguiente:

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>Spotipy Recommender</title>
  </head>
  <body>
    <h1>Autenticación</h1>

    <form action="" method="POST">
      <input type="text" id="cl_id" name="cl_id" placeholder="Client ID"><br>
      <input type="text" id="cl_secret" name="cl_secret" placeholder="Client Secret"><br>
      <input type="text" id="username" name="username" placeholder="Username"><br>
      <button type="submit" class="btn btn-dark contacto">Generar Playlist...</button>
    </form>

  <!-- Imprimir mensajes -->
  {% for message in get_flashed_messages() %}
    <p>{{ message }}</p>
  {% endfor %}
  </body>
</html>

Veamos un poco más en detalle las porciones de código más relevantes de este archivo, comenzando con el cuadro de diálogo (ubicado en el tag form):

    <form action="" method="POST">
      <input type="text" id="cl_id" name="cl_id" placeholder="Client ID"><br>
      <input type="text" id="cl_secret" name="cl_secret" placeholder="Client Secret"><br>
      <input type="text" id="username" name="username" placeholder="Username"><br>
      <button type="submit" class="btn btn-dark contacto">Generar Playlist...</button>
    </form>

Este cuadro de diálogo inicia con el método POST (<form action="" method="POST">) que indica simplemente que aceptará los datos que introduzcamos en el formulario y los enviará al aplicativo que construyamos con Flask.

La información la introduciremos como tal en los campos definidos por el tag input y acá lo que nos interesa es que para diferenciar uno de otro tendremos en cuenta su atributo id: cl_id, cl_secret y username. Estos identificadores serán los que usaremos para integrar esta página HTML con la aplicación en Flask.

Y por último en este cuadro de diálogo agregaremos un botón (etiqueta button) que permitirá enviar la información al aplicativo.

Adicionalmente, en la última parte de esta página web se incluye un bloque de código que permitirá imprimir un mensaje de notificación al usuario una vez se haya creado la playlist en Spotify:

  {% for message in get_flashed_messages() %}
    <p>{{ message }}</p>
  {% endfor %}

donde el método get_flashed_messages() permite tomar el mensaje indicado en la aplicación que construiremos en Flask y <p>{{ message }}</p> permitirá mostrarlo en la página web.

Con la página HTML ya construida lo único que nos resta es implementar el elemento central del aplicativo web: la aplicación en Flask, que permitirá conectar la información introducida por el usuario en la página web con el backend implementado anteriormente.

La aplicación en Flask (archivo app.py)

Para construir esta aplicación comenzaremos importando las librerías requeridas. En particular, del módulo Flask importaremos las funciones:

Y adicionalmente tenemos que importar el backend que se encuentra en el módulo spotipy_client:

from flask import Flask, flash, request, render_template
from spotipy_client import *

A continuación definiremos las variables REDIRECT_URI y SCOPE necesarias para la autenticación ante la API de Spotify:

REDIRECT_URI = "http://127.0.0.1:5000/api_callback"
SCOPE = 'playlist-modify-private,playlist-modify-public,user-top-read'

Y ahora sí estamos listos para construir los elementos centrales de nuestro aplicativo. Lo primero que debemos hacer es crear un objeto tipo Flask que contendrá toda la información de nuestra aplicación, así como definir una secret key que es simplemente una cadena aleatoria de bytes (y que es un requisito establecido por Flask para poder interactuar con nuestro aplicativo):

app = Flask(__name__)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'

A continuación implementamos los elementos que nos permitirán interactuar con la aplicación. Usaremos el método route para especificar la dirección URL que tendrá por defecto nuestra aplicación una vez la ejecutemos en el navegador de Internet:

@app.route('/', methods=["GET","POST"])

Ahora implementamos la función client_auth_form() que permitirá recibir datos en la página HTML, conectar el aplicativo con el backend y generar la playlist en Spotify:

def client_auth_form():
    if request.method == "POST":
        CLI_ID = request.form['cl_id']
        CLI_SEC = request.form['cl_secret']
        USERNAME = request.form['username']

        # Realizar autenticación
        sp = SpotipyClient(CLI_ID, CLI_SEC, USERNAME, REDIRECT_URI, SCOPE)

        # Generar Playlist y redireccionar
        sp.create_recommended_playlist()
        flash('¡Playlist creada en Spotify!')
    return render_template('home.html')

Analicemos en detalle las anteriores líneas de código:

¡Y listo, con esto ya tenemos implementado nuestro aplicativo web!

Prueba del aplicativo

Lo único que nos resta en este tutorial es poner a prueba el aplicativo que acabamos de implementar.

Para ello abrimos el Terminal (en Mac) o la ventana de comandos (en Windows), nos movemos al directorio que contiene el aplicativo (es decir el archivo app.py) y simplemente escribimos:

python app.py

Automáticamente al ejecutar este código aparecerá impreso en pantalla un mensaje similar a este:

*Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

que nos indica la URL local (http://127.0.0.1:5000/) desde la cual podemos acceder a nuestro aplicativo. Así que copiamos esta URL y la pegamos en la barra de direcciones del navegador y al hacerlo ya podremos interactuar con nuestro aplicativo web.

Simplemente introducimos nuestras credenciales de acceso (Client ID, Client Secret y Username), oprimimos el botón Generar playlist…, esperamos unos segundos y una vez veamos el mensaje ¡Playlist creada en Spotify! vamos a nuestra aplicación de Spotify y encontraremos que efectivamente habremos creado nuestra lista de canciones sugeridas generada por este sistema de recomendación.

Conclusión

Muy bien, en este tutorial hemos visto el desarrollo de un sistema de recomendación que permite generar una lista de reproducción con canciones afines a los gustos de un usuario en Spotify.

En la primera parte vimos cómo interactuar con la API de Spotify, mientras que en la segunda aprendimos a generar el listado de canciones a incluir en la playlist usando la similitud del coseno y el filtrado basado en contenido.

Y en esta última parte del tutorial vimos como integrar estos elementos en un aplicativo web que facilita la interacción y que nos permite de forma muy sencilla generar la lista de reproducción en la aplicación de Spotify.

Si tienes alguna duda de este artículo o tienes alguna sugerencia no dudes en contactarme diligenciando el siguiente formulario:

Debes introducir tu nombre.
Debes introducir tu apellido.
Debes introducir un email válido.
Debes introducir un mensaje.
Debes aceptar la política de privacidad.

Código fuente

En este enlace de Github está disponible el código fuente de este tutorial.