mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 17:16:07 +01:00
Merge pull request #4352 from arsaboo/spotify_extend
Extend Spotify plugin to obtain (popularity and audio features) track attributes
This commit is contained in:
commit
c25e7ad511
3 changed files with 129 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
# This file is part of beets.
|
# This file is part of beets.
|
||||||
# Copyright 2019, Rahul Ahuja.
|
# Copyright 2019, Rahul Ahuja.
|
||||||
|
# Copyright 2022, Alok Saboo.
|
||||||
#
|
#
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining
|
# Permission is hereby granted, free of charge, to any person obtaining
|
||||||
# a copy of this software and associated documentation files (the
|
# a copy of this software and associated documentation files (the
|
||||||
|
|
@ -19,6 +20,7 @@ Spotify playlist construction.
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
|
|
@ -41,6 +43,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
|
||||||
search_url = 'https://api.spotify.com/v1/search'
|
search_url = 'https://api.spotify.com/v1/search'
|
||||||
album_url = 'https://api.spotify.com/v1/albums/'
|
album_url = 'https://api.spotify.com/v1/albums/'
|
||||||
track_url = 'https://api.spotify.com/v1/tracks/'
|
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
|
# Spotify IDs consist of 22 alphanumeric characters
|
||||||
# (zero-left-padded base62 representation of randomly generated UUID4)
|
# (zero-left-padded base62 representation of randomly generated UUID4)
|
||||||
|
|
@ -49,6 +52,21 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
|
||||||
'match_group': 2,
|
'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):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config.add(
|
self.config.add(
|
||||||
|
|
@ -356,6 +374,7 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
|
||||||
return response_data
|
return response_data
|
||||||
|
|
||||||
def commands(self):
|
def commands(self):
|
||||||
|
# autotagger import command
|
||||||
def queries(lib, opts, args):
|
def queries(lib, opts, args):
|
||||||
success = self._parse_opts(opts)
|
success = self._parse_opts(opts)
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -382,7 +401,22 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
spotify_cmd.func = queries
|
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):
|
def _parse_opts(self, opts):
|
||||||
if opts.mode:
|
if opts.mode:
|
||||||
|
|
@ -536,3 +570,54 @@ class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
|
||||||
self._log.warning(
|
self._log.warning(
|
||||||
f'No {self.data_source} tracks found from beets query'
|
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
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ Changelog goes here!
|
||||||
|
|
||||||
New features:
|
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
|
* :doc:`/plugins/spotify`: The plugin now records Spotify-specific IDs in the
|
||||||
`spotify_album_id`, `spotify_artist_id`, and `spotify_track_id` fields.
|
`spotify_album_id`, `spotify_artist_id`, and `spotify_track_id` fields.
|
||||||
:bug:`4348`
|
:bug:`4348`
|
||||||
|
|
|
||||||
|
|
@ -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 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 check which tracks in your library are available on Spotify.
|
||||||
* You want to autotag music with metadata from the Spotify API.
|
* 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
|
Basic Usage
|
||||||
-----------
|
-----------
|
||||||
|
|
@ -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``
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue