The Spotify Web API can return over a dozen audio features for a track, notably tempo
- "The overall estimated tempo of a track in beats per minute (BPM)."
Given a Spotify ID, Spotipy's audio_features
method can be called as follows:
"""
Artist: Above & Beyond, Richard Bedford
Track: Sun & Moon
Track link: https://open.spotify.com/track/2CG1FmeprsyjgHIPNMYCf4
Track ID: 2CG1FmeprsyjgHIPNMYCf4
"""
sun_and_moon_id = '2CG1FmeprsyjgHIPNMYCf4'
audio_features = sp.audio_features(sun_and_moon_id)
print(json.dumps(audio_features, indent=2))
Nice! Looks like the "tempo
", or BPM, of this track is around 133. Let's continue.
Conveniently, the entire back catalogue of A State of Trance - 950+ episodes - has been uploaded to Spotify under the artist "Armin van Buuren ASOT Radio". Spotipy's artist_albums
method can list them for us, courtesy spotipy/examples/artist_albums.py:
"""
Artist: Armin van Buuren ASOT Radio
Artist link: https://open.spotify.com/artist/25mFVpuABa9GkGcj9eOPce
Artist ID: 25mFVpuABa9GkGcj9eOPce
"""
asot_radio_id = '25mFVpuABa9GkGcj9eOPce'
albums = []
results = sp.artist_albums(asot_radio_id, album_type='album')
albums.extend(results['items'])
while results['next']:
results = sp.next(results)
albums.extend(results['items'])
seen = set() # to avoid dups
for album in albums:
name = album['name']
if name not in seen:
seen.add(name)
albums.sort(key=lambda x: x['release_date']) # Sort by release date
Cool, our list albums
should now contain every episode of A State of Trance! Let's take a quick look..
for album in albums[:10]:
print(album['name'])
Hm, aren't we missing a few?
len(albums)
For some reason 25 early episodes are classified as "Singles and EPs". Let's grab those as well, and add them to the list.
"""
Artist: Armin van Buuren ASOT Radio
Artist link: https://open.spotify.com/artist/25mFVpuABa9GkGcj9eOPce
Artist ID: 25mFVpuABa9GkGcj9eOPce
"""
asot_radio_id = '25mFVpuABa9GkGcj9eOPce'
singles = []
results = sp.artist_albums(asot_radio_id, album_type='single')
singles.extend(results['items'])
while results['next']:
results = sp.next(results)
singles.extend(results['items'])
seen = set() # to avoid dups
for single in singles:
name = single['name']
if name not in seen:
seen.add(name)
episodes = singles + albums
episodes.sort(key=lambda x: x['release_date']) # Sort by release date
for episode in episodes[:10]:
print(episode['name'])
Nice!
len(episodes)
Great, that's every available episode as of writing. Let's see what we can do with all this, starting with a tracklist courtesy of Spotipy's album_tracks
method:
for track in sp.album_tracks(episodes[1]['uri'])['items']:
print(track['artists'][0]['name'], '-', track['name'])
Seems most of the early episodes are missing a bunch of tracks unfortunately, A State of Trance's website reports twice as many tracks in this episode and we'll want to remove the intro and outro as well.
Looking at a more recent episode:
for track in sp.album_tracks(episodes[945]['uri'])['items']:
track_artist = track['artists'][0]['name']
for artist in track['artists'][1:]:
track_artist += " & " + artist['name']
print(track_artist, '-', track['name'])
The more recent episodes feature a Spotify exclusive - voiceover interludes! Seems they all contain "A State of Trance" though, same with the regular intros and outros.
Without them:
episode_tracks = sp.album_tracks(episodes[945]['uri'])['items']
pruned_tracks = []
for track in episode_tracks:
if "a state of trance" in track['name'].lower() or "- interview" in track['name'].lower():
continue
else:
pruned_tracks.append(track)
track_artist = track['artists'][0]['name']
for artist in track['artists'][1:]:
track_artist += " & " + artist['name']
print(track_artist, '-', track['name'])
Much better! Finally, for fun, let's track this episode's BPM over time using some visualization libraries:
import altair as alt
import numpy as np
import pandas as pd
bpm = []
for track in pruned_tracks:
bpm.append(sp.audio_features(track['uri'])[0]['tempo'])
x = np.arange(len(pruned_tracks))
source = pd.DataFrame({
'track': x,
'bpm': np.array(bpm)
})
alt.Chart(source).mark_line().encode(
alt.X('track'),
alt.Y('bpm', scale=alt.Scale(domain=(120, 150))),
).properties(
title="ASOT 950 Part 2 - BPM of track"
)
Not great, but it gets the point across.
Now, let's get exploring! ..