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!
Tabla de contenido
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:
- El archivo
app.py
será como tal el aplicativo o frontend implementado en Flask - En el sub-directorio
spotipy_client
se encontrará el backend, es decir el sistema de recomendación. La implementación de este sistema se encontrará en el archivospotipy_client.py
, mientras que el archivo__init__.py
simplemente nos permitirá importar el sistema de recomendación como un módulo desdeapp.py
- En el sub-directorio
templates
incluiremos un sencillo archivo en formato HTML (home.html
) que definirá la apariencia y parte de la funcionalidad de nuestro aplicativo.
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:
Así que esta página contendrá:
- Unos cuadros de diálogo que permitirán introducir los datos de autenticación del usuario en la API de Spotify (Client ID, Client Secret y Username)
- Un botón que permitirá ejecutar todo el código del backend para generar la lista de reproducción en Spotify
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:
Flask
que contendrá como tal el aplicativoflash
que permitirá mostrar el mensaje'¡Playlist creada en Spotify!'
en la página web una vez hayamos creado la playlistrequest
que permitirá extraer la información introducida por el usuario en los cuadros de diálogorender_template
que permitirá mostrar la página HTML en nuestro navegador
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:
if request.method == "POST":
le indica a Flask que las siguientes líneas se ejecutarán sólo si el usuario introdujo datos en la página HTML y oprimió el botón correspondienteCLI_ID = request.form['cl_id']
,CLI_SEC = request.form['cl_secret']
yUSERNAME = request.form['username']
permite extraer la información introducida por el usuario en la página web (cl_id
,cl_secret
yusername
) y almacenarla en las respectivas variables de Python (CLI_ID
,CLI_SEC
yUSERNAME
)sp = SpotipyClient(CLI_ID, CLI_SEC, USERNAME, REDIRECT_URI, SCOPE)
permite tomar las variables extraídas de la página HTML y realizar la autenticación usando la claseSpotipyClient
del backendsp.create_recommended_playlist()
nos permite usar el backend para generar la lista de reproducción en Spotify- Y por último
flash('¡Playlist creada en Spotify!')
permite mostrar en pantalla, en la misma página HTML, el mensaje'¡Playlist creada en Spotify!'
una vez se haya ejecutado el código anterior.
¡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:
Código fuente
En este enlace de Github está disponible el código fuente de este tutorial.