From b1ad49a05459f2378a9e959f7186e77849de2256 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 12 May 2022 20:09:40 -0400 Subject: [PATCH 01/23] Update spotify.py --- beetsplug/spotify.py | 105 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index eff717a9f..e0907875b 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -356,6 +356,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 +383,23 @@ 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 or self.config['force']) + + sync_cmd.func = func + return [spotify_cmd, sync_cmd] def _parse_opts(self, opts): if opts.mode: @@ -536,3 +553,89 @@ 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.""" + spotify_audio_features = { + 'acousticness': ['spotify_track_acousticness'], + 'danceability': ['spotify_track_danceability'], + 'energy': ['spotify_track_energy'], + 'instrumentalness': ['spotify_track_instrumentalness'], + 'key': ['spotify_track_key'], + 'liveness': ['spotify_track_liveness'], + 'loudness': ['spotify_track_loudness'], + 'mode': ['spotify_track_mode'], + 'speechiness': ['spotify_track_speechiness'], + 'tempo': ['spotify_track_tempo'], + 'time_signature': ['spotify_track_time_sig'], + 'valence': ['spotify_track_valence'], + } + import time + + no_items = len(items) + self._log.info('Total {} tracks', no_items) + + for index, item in enumerate(items, start=1): + time.sleep(.5) + self._log.info('Processing {}/{} tracks - {} ', + index, no_items, item) + try: + # If we're not forcing re-downloading for all tracks, check + # whether the popularity data is already present + if not force: + spotify_track_popularity = \ + item.get('spotify_track_popularity', '') + if spotify_track_popularity: + self._log.debug('Popularity already present for: {}', + item) + continue + + popularity = self.track_popularity(item.spotify_track_id) + item['spotify_track_popularity'] = popularity + audio_features = \ + self.track_audio_features(item.spotify_track_id) + for feature in audio_features.keys(): + if feature in spotify_audio_features.keys(): + item[spotify_audio_features[feature][0]] = \ + audio_features[feature] + item.store() + if write: + item.try_write() + except AttributeError: + self._log.debug('No track_id present for: {}', item) + pass + + def track_popularity(self, track_id=None): + """Fetch a track popularity by its Spotify ID. + :param track_id: (Optional) Spotify ID or URL for the track. Either + ``track_id`` or ``track_data`` must be provided. + :type track_id: str + :param track_data: (Optional) Simplified track object dict. May be + provided instead of ``track_id`` to avoid unnecessary API calls. + :type track_data: dict + :return: TrackInfo object for track + :rtype: beets.autotag.hooks.TrackInfo or None + """ + track_data = self._handle_response( + requests.get, self.track_url + track_id + ) + self._log.debug('track_data: {}', track_data['popularity']) + track_popularity = track_data['popularity'] + return track_popularity + + def track_audio_features(self, track_id=None): + """Fetch track features by its Spotify ID. + :param track_id: (Optional) Spotify ID or URL for the track. Either + ``track_id`` or ``track_data`` must be provided. + :type track_id: str + :param track_data: (Optional) Simplified track object dict. May be + provided instead of ``track_id`` to avoid unnecessary API calls. + :type track_data: dict + :return: TrackInfo object for track + :rtype: beets.autotag.hooks.TrackInfo or None + """ + track_data = self._handle_response( + requests.get, self.audio_features_url + track_id + ) + audio_features = track_data + return audio_features From e8de749eafa76091ce02caada8ffc0ecb5f22dfb Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 12 May 2022 20:24:40 -0400 Subject: [PATCH 02/23] Clean up docstrings --- beetsplug/spotify.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index e0907875b..efb5e8381 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -606,16 +606,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): pass def track_popularity(self, track_id=None): - """Fetch a track popularity by its Spotify ID. - :param track_id: (Optional) Spotify ID or URL for the track. Either - ``track_id`` or ``track_data`` must be provided. - :type track_id: str - :param track_data: (Optional) Simplified track object dict. May be - provided instead of ``track_id`` to avoid unnecessary API calls. - :type track_data: dict - :return: TrackInfo object for track - :rtype: beets.autotag.hooks.TrackInfo or None - """ + """Fetch a track popularity by its Spotify ID.""" track_data = self._handle_response( requests.get, self.track_url + track_id ) @@ -624,16 +615,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): return track_popularity def track_audio_features(self, track_id=None): - """Fetch track features by its Spotify ID. - :param track_id: (Optional) Spotify ID or URL for the track. Either - ``track_id`` or ``track_data`` must be provided. - :type track_id: str - :param track_data: (Optional) Simplified track object dict. May be - provided instead of ``track_id`` to avoid unnecessary API calls. - :type track_data: dict - :return: TrackInfo object for track - :rtype: beets.autotag.hooks.TrackInfo or None - """ + """Fetch track audio features by its Spotify ID.""" track_data = self._handle_response( requests.get, self.audio_features_url + track_id ) From ba3a582483186ebe6291e5e67eb7f1318fa874f8 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 12 May 2022 20:36:04 -0400 Subject: [PATCH 03/23] Update spotify.py --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index efb5e8381..20b133257 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -41,6 +41,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) From db1c77fb25bc70465d4e258991fa5d067b909368 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 12 May 2022 20:38:01 -0400 Subject: [PATCH 04/23] Update spotify.py --- beetsplug/spotify.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 20b133257..47ef6148a 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -597,6 +597,8 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): self.track_audio_features(item.spotify_track_id) for feature in audio_features.keys(): if feature in spotify_audio_features.keys(): + self._log.info('feature: {}', + audio_features[feature]) item[spotify_audio_features[feature][0]] = \ audio_features[feature] item.store() From 9c9f52b7e5f17565b272bb74b7473c8b6c3108e3 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 12 May 2022 20:40:03 -0400 Subject: [PATCH 05/23] remove logging --- beetsplug/spotify.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 47ef6148a..20b133257 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -597,8 +597,6 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): self.track_audio_features(item.spotify_track_id) for feature in audio_features.keys(): if feature in spotify_audio_features.keys(): - self._log.info('feature: {}', - audio_features[feature]) item[spotify_audio_features[feature][0]] = \ audio_features[feature] item.store() From 4c4cafbf04be9f1b17643d7b73a2563db9ddba6e Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 12 May 2022 20:42:13 -0400 Subject: [PATCH 06/23] Update spotify.py --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 20b133257..58bbc89ad 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 From d465471308da7b6eb63652969d863a7970a670c5 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 13 May 2022 08:42:17 -0400 Subject: [PATCH 07/23] Add force option in config --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 58bbc89ad..d4706ba63 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -66,6 +66,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): 'client_id': '4e414367a1d14c75a5c5129a627fcab8', 'client_secret': 'f82bdc09b2254f1a8286815d02fd46dc', 'tokenfile': 'spotify_token.json', + 'force': False, } ) self.config['client_secret'].redact = True From 4eb83e8d976111fc20547ca58a7f5dfd9a6684cc Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 13 May 2022 09:58:12 -0400 Subject: [PATCH 08/23] Save track popularity during the import to save an API call --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index d4706ba63..0babe749c 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -251,6 +251,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): artist=artist, artist_id=artist_id, spotify_artist_id=artist_id, + spotify_track_popularity=track_data['popularity'], length=track_data['duration_ms'] / 1000, index=track_data['track_number'], medium=track_data['disc_number'], From f6c0bdac75e700bac1070570897a8fc75b3e97f1 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 13 May 2022 10:03:08 -0400 Subject: [PATCH 09/23] revert --- beetsplug/spotify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 0babe749c..d4706ba63 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -251,7 +251,6 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): artist=artist, artist_id=artist_id, spotify_artist_id=artist_id, - spotify_track_popularity=track_data['popularity'], length=track_data['duration_ms'] / 1000, index=track_data['track_number'], medium=track_data['disc_number'], From 80843d772050eb34e42cf3027a0cdb819d6179af Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 13 May 2022 10:08:56 -0400 Subject: [PATCH 10/23] Update spotify.py --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index d4706ba63..30ef8fafa 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -251,6 +251,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): artist=artist, artist_id=artist_id, spotify_artist_id=artist_id, + spotify_track_popularity=track_data['tracks']['popularity'], length=track_data['duration_ms'] / 1000, index=track_data['track_number'], medium=track_data['disc_number'], From 72e037f1ed3ea71b5d0d1ec302ec04b208266c67 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 13 May 2022 10:13:15 -0400 Subject: [PATCH 11/23] Update spotify.py --- beetsplug/spotify.py | 1 - 1 file changed, 1 deletion(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 30ef8fafa..d4706ba63 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -251,7 +251,6 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): artist=artist, artist_id=artist_id, spotify_artist_id=artist_id, - spotify_track_popularity=track_data['tracks']['popularity'], length=track_data['duration_ms'] / 1000, index=track_data['track_number'], medium=track_data['disc_number'], From c66225708eaf1d7944ec8ae45c89b0f4a1d3a569 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 14:50:14 -0400 Subject: [PATCH 12/23] Update beetsplug/spotify.py Co-authored-by: Adrian Sampson --- beetsplug/spotify.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index d4706ba63..8ac7755ea 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -586,9 +586,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): # If we're not forcing re-downloading for all tracks, check # whether the popularity data is already present if not force: - spotify_track_popularity = \ - item.get('spotify_track_popularity', '') - if spotify_track_popularity: + if 'spotify_track_popularity' in item: self._log.debug('Popularity already present for: {}', item) continue From d313da2765888af914100fef390cc2c7dcd3597a Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 14:50:27 -0400 Subject: [PATCH 13/23] Update beetsplug/spotify.py Co-authored-by: Adrian Sampson --- beetsplug/spotify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 8ac7755ea..868a2c0f8 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -620,5 +620,4 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): track_data = self._handle_response( requests.get, self.audio_features_url + track_id ) - audio_features = track_data - return audio_features + return track_data From 39600bcbbbb3ec143e9f36f1bfa7e1d7ab1c35e7 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 14:50:34 -0400 Subject: [PATCH 14/23] Update beetsplug/spotify.py Co-authored-by: Adrian Sampson --- beetsplug/spotify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 868a2c0f8..460f57c1e 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -612,8 +612,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): requests.get, self.track_url + track_id ) self._log.debug('track_data: {}', track_data['popularity']) - track_popularity = track_data['popularity'] - return track_popularity + return track_data['popularity'] def track_audio_features(self, track_id=None): """Fetch track audio features by its Spotify ID.""" From 9420cf4c6c03bd6a9ceb87c8ce1a3d8b0f004bdc Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 15:04:45 -0400 Subject: [PATCH 15/23] Address comments --- beetsplug/spotify.py | 56 +++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 460f57c1e..571def87c 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -20,6 +20,7 @@ Spotify playlist construction. import re import json import base64 +import time import webbrowser import collections @@ -51,6 +52,21 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): 'match_group': 2, } + spotify_audio_features = { + 'acousticness': 'spotify_track_acousticness', + 'danceability': 'spotify_track_danceability', + 'energy': 'spotify_track_energy', + 'instrumentalness': 'spotify_track_instrumentalness', + 'key': 'spotify_track_key', + 'liveness': 'spotify_track_liveness', + 'loudness': 'spotify_track_loudness', + 'mode': 'spotify_track_mode', + 'speechiness': 'spotify_track_speechiness', + 'tempo': 'spotify_track_tempo', + 'time_signature': 'spotify_track_time_sig', + 'valence': 'spotify_track_valence', + } + def __init__(self): super().__init__() self.config.add( @@ -66,7 +82,6 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): 'client_id': '4e414367a1d14c75a5c5129a627fcab8', 'client_secret': 'f82bdc09b2254f1a8286815d02fd46dc', 'tokenfile': 'spotify_token.json', - 'force': False, } ) self.config['client_secret'].redact = True @@ -559,45 +574,28 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): def _fetch_info(self, items, write, force): """Obtain track information from Spotify.""" - spotify_audio_features = { - 'acousticness': ['spotify_track_acousticness'], - 'danceability': ['spotify_track_danceability'], - 'energy': ['spotify_track_energy'], - 'instrumentalness': ['spotify_track_instrumentalness'], - 'key': ['spotify_track_key'], - 'liveness': ['spotify_track_liveness'], - 'loudness': ['spotify_track_loudness'], - 'mode': ['spotify_track_mode'], - 'speechiness': ['spotify_track_speechiness'], - 'tempo': ['spotify_track_tempo'], - 'time_signature': ['spotify_track_time_sig'], - 'valence': ['spotify_track_valence'], - } - import time - no_items = len(items) - self._log.info('Total {} tracks', no_items) + self._log.debug('Total {} tracks', len(items)) for index, item in enumerate(items, start=1): time.sleep(.5) self._log.info('Processing {}/{} tracks - {} ', - index, no_items, item) + 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: - # 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 - popularity = self.track_popularity(item.spotify_track_id) item['spotify_track_popularity'] = popularity audio_features = \ self.track_audio_features(item.spotify_track_id) for feature in audio_features.keys(): - if feature in spotify_audio_features.keys(): - item[spotify_audio_features[feature][0]] = \ + if feature in self.spotify_audio_features.keys(): + item[self.spotify_audio_features[feature]] = \ audio_features[feature] item.store() if write: From 19e2a11ea0050e4dbfe93d688c472741a816ba31 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 15:30:51 -0400 Subject: [PATCH 16/23] Updated documents and changelog. --- docs/changelog.rst | 4 ++++ docs/plugins/spotify.rst | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 72b1cf1fe..c67d57389 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 c8e2bfb83..d29783fc6 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,19 @@ Here's an example:: } ] +Obtaining Track Popularity and Audio Features from Spotify +---------------------------------------------------------- + +Spotify provides track `popularity`_ and audio `features`_ that be used to +create better playlists. + +.. _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. From b2a90bf089e0c85357ad538ee2e0ba307b0de442 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 15:43:16 -0400 Subject: [PATCH 17/23] Changed spotify labels based on comment --- beetsplug/spotify.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 571def87c..e8d896f08 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -53,18 +53,18 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): } spotify_audio_features = { - 'acousticness': 'spotify_track_acousticness', - 'danceability': 'spotify_track_danceability', - 'energy': 'spotify_track_energy', - 'instrumentalness': 'spotify_track_instrumentalness', - 'key': 'spotify_track_key', - 'liveness': 'spotify_track_liveness', - 'loudness': 'spotify_track_loudness', - 'mode': 'spotify_track_mode', - 'speechiness': 'spotify_track_speechiness', - 'tempo': 'spotify_track_tempo', - 'time_signature': 'spotify_track_time_sig', - 'valence': 'spotify_track_valence', + '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): From 28a4e43b588a01e8cdbaec800863ce0b90f2fc8c Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 15:58:28 -0400 Subject: [PATCH 18/23] Clarified documentation --- docs/plugins/spotify.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/plugins/spotify.rst b/docs/plugins/spotify.rst index d29783fc6..169846adf 100644 --- a/docs/plugins/spotify.rst +++ b/docs/plugins/spotify.rst @@ -121,4 +121,8 @@ 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. + 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. From f77a146f17c57c361d958b220c883819105466d0 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 20:52:30 -0400 Subject: [PATCH 19/23] remove force config option --- beetsplug/spotify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index e8d896f08..90484fc59 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -413,8 +413,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): def func(lib, opts, args): items = lib.items(ui.decargs(args)) - self._fetch_info(items, ui.should_write(), - opts.force_refetch or self.config['force']) + self._fetch_info(items, ui.should_write(), opts.force_refetch) sync_cmd.func = func return [spotify_cmd, sync_cmd] From b9685a4784343288cfe7e39a0851c4163f639ce5 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 17 May 2022 21:07:57 -0400 Subject: [PATCH 20/23] Add more details to the docs --- docs/plugins/spotify.rst | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/plugins/spotify.rst b/docs/plugins/spotify.rst index 169846adf..6a4119984 100644 --- a/docs/plugins/spotify.rst +++ b/docs/plugins/spotify.rst @@ -109,10 +109,11 @@ Here's an example:: Obtaining Track Popularity and Audio Features from Spotify ---------------------------------------------------------- -Spotify provides track `popularity`_ and audio `features`_ that be used to -create better playlists. +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 @@ -126,3 +127,19 @@ these track attributes from Spotify: 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`` From 3cd6fd64ca703b692d06d51ff106881e795a99ab Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 12 Jun 2022 13:30:23 -0400 Subject: [PATCH 21/23] Added comment about sleep --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 90484fc59..b39343284 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -577,6 +577,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): self._log.debug('Total {} tracks', len(items)) for index, item in enumerate(items, start=1): + # Added sleep to avoid API rate limit time.sleep(.5) self._log.info('Processing {}/{} tracks - {} ', index, len(items), item) From 3d917edd67a0a13b05e0f8590a5648a0bfc5b8c9 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 12 Jun 2022 13:31:40 -0400 Subject: [PATCH 22/23] Update spotify.py --- beetsplug/spotify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index b39343284..66052f5fe 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -578,6 +578,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): 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) From 9a392f3157978c38757bcdfa77268471510df49a Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 12 Jun 2022 13:58:08 -0400 Subject: [PATCH 23/23] Address try/except comment --- beetsplug/spotify.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 66052f5fe..d7062f7d4 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -590,20 +590,22 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): item) continue try: - popularity = self.track_popularity(item.spotify_track_id) - item['spotify_track_popularity'] = popularity - audio_features = \ - self.track_audio_features(item.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() + spotify_track_id = item.spotify_track_id except AttributeError: self._log.debug('No track_id present for: {}', item) - pass + 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."""