Merge pull request #4352 from arsaboo/spotify_extend

Extend Spotify plugin to obtain (popularity and audio features) track attributes
This commit is contained in:
Adrian Sampson 2022-06-12 17:33:29 -04:00 committed by GitHub
commit c25e7ad511
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 129 additions and 2 deletions

View file

@ -1,5 +1,6 @@
# This file is part of beets.
# Copyright 2019, Rahul Ahuja.
# Copyright 2022, Alok Saboo.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -19,6 +20,7 @@ Spotify playlist construction.
import re
import json
import base64
import time
import webbrowser
import collections
@ -41,6 +43,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
search_url = 'https://api.spotify.com/v1/search'
album_url = 'https://api.spotify.com/v1/albums/'
track_url = 'https://api.spotify.com/v1/tracks/'
audio_features_url = 'https://api.spotify.com/v1/audio-features/'
# Spotify IDs consist of 22 alphanumeric characters
# (zero-left-padded base62 representation of randomly generated UUID4)
@ -49,6 +52,21 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
'match_group': 2,
}
spotify_audio_features = {
'acousticness': 'spotify_acousticness',
'danceability': 'spotify_danceability',
'energy': 'spotify_energy',
'instrumentalness': 'spotify_instrumentalness',
'key': 'spotify_key',
'liveness': 'spotify_liveness',
'loudness': 'spotify_loudness',
'mode': 'spotify_mode',
'speechiness': 'spotify_speechiness',
'tempo': 'spotify_tempo',
'time_signature': 'spotify_time_signature',
'valence': 'spotify_valence',
}
def __init__(self):
super().__init__()
self.config.add(
@ -356,6 +374,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
return response_data
def commands(self):
# autotagger import command
def queries(lib, opts, args):
success = self._parse_opts(opts)
if success:
@ -382,7 +401,22 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
),
)
spotify_cmd.func = queries
return [spotify_cmd]
# spotifysync command
sync_cmd = ui.Subcommand('spotifysync',
help="fetch track attributes from Spotify")
sync_cmd.parser.add_option(
'-f', '--force', dest='force_refetch',
action='store_true', default=False,
help='re-download data when already present'
)
def func(lib, opts, args):
items = lib.items(ui.decargs(args))
self._fetch_info(items, ui.should_write(), opts.force_refetch)
sync_cmd.func = func
return [spotify_cmd, sync_cmd]
def _parse_opts(self, opts):
if opts.mode:
@ -536,3 +570,54 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
self._log.warning(
f'No {self.data_source} tracks found from beets query'
)
def _fetch_info(self, items, write, force):
"""Obtain track information from Spotify."""
self._log.debug('Total {} tracks', len(items))
for index, item in enumerate(items, start=1):
# Added sleep to avoid API rate limit
# https://developer.spotify.com/documentation/web-api/guides/rate-limits/
time.sleep(.5)
self._log.info('Processing {}/{} tracks - {} ',
index, len(items), item)
# If we're not forcing re-downloading for all tracks, check
# whether the popularity data is already present
if not force:
if 'spotify_track_popularity' in item:
self._log.debug('Popularity already present for: {}',
item)
continue
try:
spotify_track_id = item.spotify_track_id
except AttributeError:
self._log.debug('No track_id present for: {}', item)
continue
popularity = self.track_popularity(spotify_track_id)
item['spotify_track_popularity'] = popularity
audio_features = \
self.track_audio_features(spotify_track_id)
for feature in audio_features.keys():
if feature in self.spotify_audio_features.keys():
item[self.spotify_audio_features[feature]] = \
audio_features[feature]
item.store()
if write:
item.try_write()
def track_popularity(self, track_id=None):
"""Fetch a track popularity by its Spotify ID."""
track_data = self._handle_response(
requests.get, self.track_url + track_id
)
self._log.debug('track_data: {}', track_data['popularity'])
return track_data['popularity']
def track_audio_features(self, track_id=None):
"""Fetch track audio features by its Spotify ID."""
track_data = self._handle_response(
requests.get, self.audio_features_url + track_id
)
return track_data

View file

@ -8,6 +8,10 @@ Changelog goes here!
New features:
* :doc:`/plugins/spotify`: The plugin now provides an additional command
`spotifysync` that allows getting track popularity and audio features
information from Spotify.
:bug:`4094`
* :doc:`/plugins/spotify`: The plugin now records Spotify-specific IDs in the
`spotify_album_id`, `spotify_artist_id`, and `spotify_track_id` fields.
:bug:`4348`

View file

@ -19,6 +19,7 @@ Why Use This Plugin?
* You have playlists or albums you'd like to make available in Spotify from Beets without having to search for each artist/album/track.
* You want to check which tracks in your library are available on Spotify.
* You want to autotag music with metadata from the Spotify API.
* You want to obtain track popularity and audio features (e.g., danceability)
Basic Usage
-----------
@ -58,7 +59,7 @@ configuration options are provided.
The default options should work as-is, but there are some options you can put
in config.yaml under the ``spotify:`` section:
- **mode**: One of the following:
- **mode**: One of the following:
- ``list``: Print out the playlist as a list of links. This list can then
be pasted in to a new or existing Spotify playlist.
@ -105,3 +106,40 @@ Here's an example::
}
]
Obtaining Track Popularity and Audio Features from Spotify
----------------------------------------------------------
Spotify provides information on track `popularity`_ and audio `features`_ that
can be used for music discovery.
.. _popularity: https://developer.spotify.com/documentation/web-api/reference/#/operations/get-track
.. _features: https://developer.spotify.com/documentation/web-api/reference/#/operations/get-audio-features
The ``spotify`` plugin provides an additional command ``spotifysync`` to obtain
these track attributes from Spotify:
* ``beet spotifysync [-f]``: obtain popularity and audio features information
for every track in the library. By default, ``spotifysync`` will skip tracks
that already have this information populated. Using the ``-f`` or ``-force``
option will download the data even for tracks that already have it. Please
note that ``spotifysync`` works on tracks that have the Spotify track
identifiers. So run ``spotifysync`` only after importing your music, during
which Spotify identifiers will be added for tracks where Spotify is chosen as
the tag source.
In addition to ``popularity``, the command currently sets these audio features
for all tracks with a Spotify track ID:
* ``acousticness``
* ``danceability``
* ``energy``
* ``instrumentalness``
* ``key``
* ``liveness``
* ``loudness``
* ``mode``
* ``speechiness``
* ``tempo``
* ``time_signature``
* ``valence``