Skip to content

Spotify: Batch Spotify spotifysync API calls#6485

Open
arsaboo wants to merge 18 commits intobeetbox:masterfrom
arsaboo:spotify_batch
Open

Spotify: Batch Spotify spotifysync API calls#6485
arsaboo wants to merge 18 commits intobeetbox:masterfrom
arsaboo:spotify_batch

Conversation

@arsaboo
Copy link
Copy Markdown
Contributor

@arsaboo arsaboo commented Mar 31, 2026

This changes the Spotify plugin to batch spotifysync lookups instead of making per-track API calls. It now:

  • batches track metadata requests through /v1/tracks
  • batches audio-features requests through /v1/audio-features
  • deduplicates repeated Spotify track IDs within a run
  • preserves the existing behavior that disables audio-features fetching after a Spotify 403

The previous implementation made one metadata request and one audio-features request per track, which was inefficient for larger libraries and increased the chance of hitting rate limits. Batching reduces request volume substantially while keeping the stored fields and user-facing behavior the same.

  • Changelog. (Add an entry to docs/changelog.rst to the bottom of one of the lists near the top of the document.)
  • Tests. (Very much encouraged but not strictly required.)

@arsaboo arsaboo requested a review from a team as a code owner March 31, 2026 23:38
Copilot AI review requested due to automatic review settings March 31, 2026 23:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

PR make Spotify spotifysync do fewer HTTP call. It batch track metadata and audio-features fetch, so big library not hit rate limit so fast.

Changes:

  • Add batch fetch for /v1/tracks (50 ids per request) and /v1/audio-features (100 ids per request).
  • Deduplicate repeated Spotify track IDs within one run of _fetch_info.
  • Add tests for chunking, endpoint usage, dedupe, and 403-disable behavior; add changelog entry.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
beetsplug/spotify.py Implement batch endpoints + ID chunking/dedupe and shared disable logic for audio features.
test/plugins/test_spotify.py Add response mocks + assertions for batching/dedupe/403 behavior.
docs/changelog.rst Add Unreleased changelog entry for Spotify batching change.

Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread docs/changelog.rst Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 83.13253% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.95%. Comparing base (e3e8793) to head (7d756d7).

Files with missing lines Patch % Lines
beetsplug/spotify.py 82.05% 6 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6485      +/-   ##
==========================================
+ Coverage   71.78%   71.95%   +0.16%     
==========================================
  Files         156      156              
  Lines       20176    20236      +60     
  Branches     3214     3226      +12     
==========================================
+ Hits        14484    14561      +77     
+ Misses       5006     4970      -36     
- Partials      686      705      +19     
Files with missing lines Coverage Δ
beets/library/migrations.py 96.49% <100.00%> (-0.12%) ⬇️
beets/util/__init__.py 79.23% <100.00%> (+0.12%) ⬆️
beetsplug/spotify.py 62.59% <82.05%> (+11.99%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JOJ0 JOJ0 added the plugin Pull requests that are plugins related label Apr 4, 2026
@snejus snejus added spotify spotify plugin and removed plugin Pull requests that are plugins related labels Apr 4, 2026
Copy link
Copy Markdown
Member

@snejus snejus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of comments

Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread docs/changelog.rst
Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated

track_ids = [track_id for _, track_id in items_to_update]
unique_track_ids = list(dict.fromkeys(track_ids))
track_info_by_id = self.track_info_batch(unique_track_ids)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
track_info_by_id = self.track_info_batch(unique_track_ids)
audio_features_by_id = self.track_info_batch(unique_track_ids)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming here would clash with audio_features_by_id already used for track_audio_features_batch. Kept as track_info_by_id for now.

Copy link
Copy Markdown
Member

@snejus snejus Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I renamed the typed dict to TrackDetails so you can have track_details_by_id here, to remove ambiguity regarding beets.hooks.info::TrackInfo.

Comment thread beetsplug/spotify.py Outdated
Comment on lines +853 to +855
popularity, isrc, ean, upc = track_info_by_id.get(
spotify_track_id, (None, None, None, None)
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, define AudioFeatures as a typed dict and return them from track_info_batch, then you can simplify this logic:

Suggested change
popularity, isrc, ean, upc = track_info_by_id.get(
spotify_track_id, (None, None, None, None)
)
if audio_features := audio_features_by_id(spotify_track_id):
item.update(**audio_features)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't work directly because the SpotifyTrackInfo field names (popularity, isrc, ean, upc) don't match the beets item field names (spotify_track_popularity, isrc, ean, upc), and for audio features the Spotify keys (danceability) also differ from beets fields (spotify_danceability).

Comment thread beetsplug/spotify.py

Unauthorized responses trigger one token refresh attempt before the
method gives up and falls back to an empty result set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Copy link
Copy Markdown
Member

@snejus snejus Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And undo all unrelated changes in the docstrings please.

Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py Outdated

track_ids = [track_id for _, track_id in items_to_update]
unique_track_ids = list(dict.fromkeys(track_ids))
track_info_by_id = self.track_info_batch(unique_track_ids)
Copy link
Copy Markdown
Member

@snejus snejus Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I renamed the typed dict to TrackDetails so you can have track_details_by_id here, to remove ambiguity regarding beets.hooks.info::TrackInfo.

Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py
"Processing {}/{} tracks - {} ", index, len(items), item
)
# If we're not forcing re-downloading for all tracks, check
# whether the popularity data is already present
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configure your AI agent to not remove stuff like this

Comment thread beetsplug/spotify.py Outdated
Comment thread beetsplug/spotify.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

spotify spotify plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants