diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index eff717a9f..d7062f7d4 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -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 diff --git a/docs/changelog.rst b/docs/changelog.rst index d6c74e451..c95287443 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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` diff --git a/docs/plugins/spotify.rst b/docs/plugins/spotify.rst index 7340e2150..233d00726 100644 --- a/docs/plugins/spotify.rst +++ b/docs/plugins/spotify.rst @@ -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``