• Saltar a la navegación principal
  • Saltar al contenido principal
Logo Codificando Bits

Codificando Bits

  • Academia
    • Rutas
    • Cursos
    • Proyectos
    • Tutoriales
  • Acceder
  • Suscribirse

Recomendador musical para Spotify – Parte 3: el aplicativo web

En esta serie de tutoriales estamos construyendo un sistema de recomendación para Spotify.

En este tercer y último 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.

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 archivo «spotipy_client.py», mientras que el archivo «__init__.py» simplemente nos permitirá importar el sistema de recomendación como un módulo desde «app.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 el segundo tutorial de esta serie:

    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á:

  • 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 aplicativo
  • «flash» que permitirá mostrar el mensaje  «‘Playlist creada en Spotify!'» en la página web una vez hayamos creado la playlist
  • «request» que permitirá extraer la información introducida por el usuario en los cuadros de diálogo
  • «render_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 «REDIRECTX_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 correspondiente
  • «CLI_ID = request.form[‘cl_id’], CLI_SEC = request.form[‘cl_secret’] y USERNAME = request.form[‘username’]»: permite extraer la información introducida por el usuario en la página web («cl_id», «cl_secret» y «username») y almacenarla en las respectivas variables de Python («CLI_ID», «CLI_SEC» y «USERNAME»)
  • «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 clase «SpotipyClient» del backend
  • «sp.create_creommended_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.

Enlace de descarga del código fuente

▼ El notebook de este tutorial

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 te interesa ver otra aplicación práctica de este tipo de sistemas, te invito a darle una mirada al proyecto Sistema de recomendación de libros usando filtrado colaborativo, disponible acá en la Academia Online.

© Codificando Bits, LLC | Blog | Contacto | Servicios | Acerca de | Políticas

¿Has olvidado la contraseña?
¿Has perdido tu contraseña? Por favor, introduce tu nombre de usuario o dirección de correo electrónico. Recibirás por correo electrónico un enlace para crear una nueva contraseña.
body::-webkit-scrollbar { width: 7px; } body::-webkit-scrollbar-track { border-radius: 10px; background: #f0f0f0; } body::-webkit-scrollbar-thumb { border-radius: 50px; background: #dfdbdb }