Skip to content

Spotify: Batch Spotify spotifysync API calls#6485

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

Spotify: Batch Spotify spotifysync API calls#6485
arsaboo wants to merge 4 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.

@staticmethod
def _chunked(ids: Sequence[str], chunk_size: int) -> list[list[str]]:
"""Split IDs into deterministic chunks for Spotify batch endpoints."""
return [ids[i : i + chunk_size] for i in range(0, len(ids), chunk_size)]
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

grug see type lie. _chunked say it return list[list[str]], but slice of Sequence[str] is Sequence[str] (often tuple). mypy likely yell. make _chunked build real list for each chunk (e.g., wrap slice with list(...)) or change return type to list[Sequence[str]] and adjust callers.

Suggested change
return [ids[i : i + chunk_size] for i in range(0, len(ids), chunk_size)]
return [
list(ids[i : i + chunk_size])
for i in range(0, len(ids), chunk_size)
]

Copilot uses AI. Check for mistakes.
Comment on lines +790 to +812
try:
for chunk in self._chunked(track_ids, 100):
features_data = self._handle_response(
"get",
self.audio_features_batch_url,
params={"ids": ",".join(chunk)},
)

for idx, feature_data in enumerate(
features_data.get("audio_features", [])
):
if feature_data is None:
continue
track_id = feature_data.get("id") or chunk[idx]
features_by_id[track_id] = feature_data
return features_by_id
except AudioFeaturesUnavailableError:
self._disable_audio_features()
return {}
except APIError as e:
self._log.debug("Spotify API error: {}", e)
return {}

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

grug think batch audio feature fetch change behavior on error. old per-track track_audio_features() catch APIError and just skip that one track. new track_audio_features_batch() catch APIError and return {} for whole run, also drop any features already fetched for earlier chunks. better: keep features_by_id so far and continue next chunk (or at least return partial dict) so one bad request no nuke all tracks.

Suggested change
try:
for chunk in self._chunked(track_ids, 100):
features_data = self._handle_response(
"get",
self.audio_features_batch_url,
params={"ids": ",".join(chunk)},
)
for idx, feature_data in enumerate(
features_data.get("audio_features", [])
):
if feature_data is None:
continue
track_id = feature_data.get("id") or chunk[idx]
features_by_id[track_id] = feature_data
return features_by_id
except AudioFeaturesUnavailableError:
self._disable_audio_features()
return {}
except APIError as e:
self._log.debug("Spotify API error: {}", e)
return {}
for chunk in self._chunked(track_ids, 100):
try:
features_data = self._handle_response(
"get",
self.audio_features_batch_url,
params={"ids": ",".join(chunk)},
)
except AudioFeaturesUnavailableError:
# audio features API now unavailable; stop further requests
self._disable_audio_features()
break
except APIError as e:
# one bad request should not nuke all tracks; log and continue
self._log.debug("Spotify API error: {}", e)
continue
for idx, feature_data in enumerate(
features_data.get("audio_features", [])
):
if feature_data is None:
continue
track_id = feature_data.get("id") or chunk[idx]
features_by_id[track_id] = feature_data
return features_by_id

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +32
Other changes
~~~~~~~~~~~~~

- :doc:`plugins/spotify`: Batch ``spotifysync`` track and audio-features API
requests and deduplicate repeated Spotify track IDs within a run.

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

grug see changelog entry sit under Bug fixes because Other changes heading still commented out (..). batching API calls not bug fix, and heading not show in built docs. please uncomment the Other changes subsection (or move entry to right Unreleased subsection) so changelog structure correct.

Copilot uses AI. Check for mistakes.
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.20%. Comparing base (e69a588) to head (75cfa0d).
⚠️ Report is 61 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/spotify.py 77.77% 6 Missing and 8 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6485      +/-   ##
==========================================
+ Coverage   69.75%   70.20%   +0.44%     
==========================================
  Files         145      146       +1     
  Lines       18508    18558      +50     
  Branches     3011     3022      +11     
==========================================
+ Hits        12911    13029     +118     
+ Misses       4969     4884      -85     
- Partials      628      645      +17     
Files with missing lines Coverage Δ
beetsplug/spotify.py 61.81% <77.77%> (+11.21%) ⬆️

... and 8 files with indirect coverage changes

🚀 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin Pull requests that are plugins related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants