Introduction

This post is a continuation of the development of my Albums to Ruin Twitter bot. This is the first part of actual development of the bot - information on the concept and outline can be found in the Initial Concept post.

In this post, I’m going to cover the process required for getting the recently played tracks from Spotify and the URL for the album art.

Getting Started

First of all, we need to register an application with Spotify. This is done via the Developer Dashboard and is relatively straightforward. This provides us with the Client ID and Client Secret keys that let us access the APIs.

While we’re here, we also need to set up a Redirect URI. For other Spotify apps, this will be the address the user is redirected to once they log in. For the time being, we’ll only be using the account we’re logged into the developer console with, so this is not critical, but we need something here to ensure we can connect properly later on.

Clicking on the application and then clicking on Edit Settings should allow you to add in a Redirect URI. Setting this to http://localhost:8888/callback gives us something we can work with later on. The settings page will look something like this:

Next, we need to set up our Python environment. For the purposes of this project, I’m going to be using the spotipy library (latest version at time of writing is 2.1.1) as it’s lightweight and has good documentation. Spotipy lets you pass your Client ID and Client Secret keys either through parameters or through environment variables - for ease of sharing the code, I’m going to use environment variables. The Spotipy documentation has good advice for setting this up, particularly for Windows. In Linux (e.g. on a Raspberry Pi), it’s a little more fiddly than I’m used to as you need to make sure it’s set permanently. I did this through ~/etc/profile. Now we should be ready to go.

Authenticating

The first step in connecting to the API is authenticating. First, we’ll request a token allowing access to the endpoint we require. Spotipy should be able to cache this token and refresh it when needed, so you should only need to call this section of the code once ever. This is the first time we need our Redirect URI, and if you want to run this code yourself you’ll need to replace my username with your own.

import spotipy.util as util

token = util.prompt_for_user_token(
    'valeadam', scope='user-read-recently-played', 
    redirect_uri='http://localhost:8888/callback'
)

If you run this, your web browser should load asking for permission to connect your application to your Spotify account. Once this is approved, we should be clear to start accessing our information.

Now we need to set up an authenticated connection. Spotipy makes this fairly easy via its Spotify and SpotifyOAuth classes. Again, if you wanted to run this you’d need to replace my username with your own.

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials


auth = spotipy.SpotifyOAuth(
    redirect_uri='http://localhost:8888/callback', username='valeadam'
)

sp = spotipy.Spotify(auth_manager=auth)
print(sp.me())

This prints out some information about the connected user:

{
    'display_name': 'valeadam',
    'external_urls': {'spotify': 'https://open.spotify.com/user/valeadam'},
    'followers': {'href': None, 'total': 39},
    'href': 'https://api.spotify.com/v1/users/valeadam',
    'id': 'valeadam',
    'images': [],
    'type': 'user',
    'uri': 'spotify:user:valeadam'
}

If it doesn’t, there may be some error in your process. Double-check your username is correct, and that the SPOTIPY_CLIENT_ID and SPOTIPY_CLIENT_SECRET environment variables are set up correctly.

Getting recently played tracks

Now we have our authenticated connection, we can use this to access our recently played tracks. This can be done through the current_user_recently_played() method of our Spotify object. This provides (a lot of) information on the 50 most recently played tracks as a Python dictionary. The method returns track-level information, but for the purposes of this bot we actually only want the album-level data. I’ve cleared out information irrelevant to the task at hand with ellipses, for a more complete example consult the Spotify web api reference for this method.The information I’ll want to access for the Twitter bot is structured as follows:

{
    'items':
        [
            {'track': {'album': {'album_type': 'album',
                                 'artists': [
                                     {'external_urls': {'spotify': '...'},
                                      'href': '...',
                                      'id': '4xYk2flbrS0hDzDLyDimbO',
                                      'name': 'Tubelord',
                                      'type': 'artist',
                                      'uri': 'spotify:artist:4xYk2flbrS0hDzDLyDimbO'
                                      }],
                                 # ...
                                 'external_urls': {'spotify': '...'},
                                 # ...
                                 'images': [{'height': 640,
                                             'url': '...',
                                             'width': 640
                                             },
                                            {'height': 300,
                                             'url': '...',
                                             'width': 300
                                             },
                                            {'height': 64,
                                             'url': '...',
                                             'width': 64
                                             }],
                                 'name': 'Our First American Friends',
                                 'release_date': '2009',
                                 'release_date_precision': 'year',
                                 'total_tracks': 10,
                                 'type': 'album',
                                 'uri': 'spotify:album:4nXbDzk9hxR8aUMkBvpV6u'
                                 },
                       # ...
                       },
             # ...
             },
             # ...
        ]
}

There’s a few things we can note from this. Firstly, there’s the possibility of multiple artists showing up on one album. It would be nice to be able simply combine them all into a comma-separated list, as this is likely how I’d want to include this information in the corresponding tweet. The only issue here is it’s unclear how compilations are dealt with - would 20 artists on a compilation each appear individually? If so, we might risk going past the tweet length limit. A simple way of avoiding this issue is to replace the returned set of artists if they exceed a certain length. We’ll arbitrarily set this to 5.

Secondly, multiple sizes of artwork are returned. For the best result, we want the largest image provided. It looks as though the images are ordered by size from largest to smallest, but it’s not too complicated to add an extra bit of processing to ensure we do indeed get the largest.

Now we can begin to construct our class for the album object. There are a few other things we might want to think about:

  • If multiple tracks are played from the same album, the album will appear in the response multiple times. As such, we some way to check if two of our objects correspond to the same album so we can remove duplicates. Doing this through the __eq__ magic method makes sense here, and allows us to directly use ==.

  • Thinking ahead for the tweets, we might also want a nice string version of the album information to include with the image. We can use another magic method for this - __str__ - so we’ll add this too.

  • If we’re going to be adding the data to a database, it’ll help to have a quick way to access all of the attributes of each album object. The __dict__ magic method does this, but isn’t a particularly nice method of access. Additionally, we may end up wanting to exclude certain attributes from the returned dictionary. Adding another method gives us this flexibilty.

Putting these things together gets us the following class:

class SpotifyAlbum:
    def __init__(self, resp_dict):
        self.name = resp_dict.get('name', None)
        self.release_date = resp_dict.get('release_date', 'Unknown')

        # Handle possibility of many artists
        artists = resp_dict['artists']
        if len(artists) > 5:
            self.artists = 'Various'
        else:
            self.artists = ', '.join(
                art.get('name', 'Unknown') for art in artists
            )
        
        urls = resp_dict.get('external_urls', {'spotify': None})
        self.link = urls['spotify']
        
        # Get the largest version of the album artwork
        images = resp_dict.get('images', [{'url': None, 'height': 0}])
        images.sort(key=lambda x: x['height'], reverse=True)
        self.art_link = images[0]['url']
    
    def __str__(self):
        return (
            f'Album Name: {self.name}\n'
            f'Artist(s): {self.artists}\n'
            f'Released: {self.release_date}\n'
            f'Link: {self.link}'
        )
    
    def __eq__(self, other):
        if type(other) is type(self):
            return self.__dict__ == other.__dict__
        return NotImplemented
    
    def to_dict(self):
        return self.__dict__

This should give us the basic amount of information we need. As a last step for this section, we might want to consider how we can get the unique albums from the most recent 50 played tracks. This API returns the most recently played track first in the list, whereas for my Twitter bot I want to have the albums posted in the order they’ve been encountered. This means reversing the items passed by the API. It also means we can’t just use set, as it ignores order. It’s not a huge issue, as we need just a few lines of code:

recently_played = sp.current_user_recently_played()

recent_albums = []

for item in recently_played['items'][::-1]:
    album = SpotifyAlbum(item['track']['album'])
    if album.art_link and not any(prev == album for prev in recent_albums):
        recent_albums.append(album)

Now, via recent_albums, we have a list of unique recently played albums in the order in which they’ve been played. This gives us the starting point for the rest of the project. Putting all the code together, we get the following:

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials


class SpotifyAlbum:
    def __init__(self, resp_dict):
        self.name = resp_dict.get('name', None)
        self.release_date = resp_dict.get('release_date', 'Unknown')
        self.artists = ', '.join(
            a.get('name', 'Unknown') for a in resp_dict['artists']
        )
        
        urls = resp_dict.get('external_urls', {'spotify': None})
        self.link = urls['spotify']
        
        images = resp_dict.get('images', [{'url': None, 'height': 0}])
        images.sort(key=lambda x: x['height'], reverse=True)
        self.art_link = images[0]['url']
    
    def __str__(self):
        return (
            f'Album: {self.name}\n'
            f'Artist(s): {self.artists}\n'
            f'Released: {self.release_date}\n'
            f'Link: {self.link}'
        )
    
    def __eq__(self, other):
        if type(other) is type(self):
            return self.__dict__ == other.__dict__
        return NotImplemented
    
    def to_dict(self):
        return self.__dict__

if __name__ == '__main__':
    auth = spotipy.SpotifyOAuth(
        redirect_uri='http://localhost:8888', username='valeadam'
    )

    sp = spotipy.Spotify(auth_manager=auth)

    recently_played = sp.current_user_recently_played()

    recent_albums = []

    for item in recently_played['items'][::-1]:
        album = SpotifyAlbum(item['track']['album'])
        if album.art_link and not any(prev == album for prev in recent_albums):
            recent_albums.append(album)