Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion beetsplug/spotify.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,6 +20,7 @@
import re
import json
import base64
import time
import webbrowser
import collections

Expand All @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -356,6 +374,7 @@ def _search_api(self, query_type, filters=None, keywords=''):
return response_data

def commands(self):
# autotagger import command
def queries(lib, opts, args):
success = self._parse_opts(opts)
if success:
Expand All @@ -382,7 +401,22 @@ def queries(lib, opts, args):
),
)
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:
Expand Down Expand Up @@ -536,3 +570,54 @@ def _output_match_results(self, results):
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)
Comment thread
wisp3rwind marked this conversation as resolved.
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
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
40 changes: 39 additions & 1 deletion docs/plugins/spotify.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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``