Skip to content

fix: serialize Spotify captures to prevent cross-download audio mixup (v0.29.0) - #48

Merged
beveradb merged 3 commits into
mainfrom
feat/sess-20260901-1844-spotify-concurrency-mixup
Sep 1, 2026
Merged

fix: serialize Spotify captures to prevent cross-download audio mixup (v0.29.0)#48
beveradb merged 3 commits into
mainfrom
feat/sess-20260901-1844-spotify-concurrency-mixup

Conversation

@beveradb

@beveradb beveradb commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

Job 842948f4 (source: Maduk - One Last Picture, Spotify) received the wrong input audio — its input/Unknown - 4LXnEERKcz4aRC4NCMQJ0x.flac actually contained Keeno - Shelter from the Storm (job 12accca1, also a Spotify source made moments earlier).

Root cause — confirmed from production logs

Spotify captures ran in a thread pool with no serialization. A Spotify account has a single active playback stream, and every capture registered a librespot Spotify Connect device under the same constant name flacfetch-capture and drove playback via the Web API on the same account.

Production timeline (flacup journal, 2026-08-31):

17:20:35  download-by-id Spotify 4LXnEERKcz4aRC4NCMQJ0x (One Last Picture) → start_playback
17:20:37  download-by-id Spotify 0S4fjRqfZP6XOF9j7vzmR1 (Shelter from the Storm) → start_playback  ← 2s later, concurrent
17:20:41  "Download complete: Unknown - 4LXnEERKcz4aRC4NCMQJ0x.flac"   ← job A filename, job B audio
17:25:20  SpotifyDownloadError: Download timeout                       ← job B starved

The second start_playback() hijacked the account's one playback stream, so job A's capture pipe recorded job B's track, and job B captured nothing and timed out. The wrong audio is undetectable downstream (valid FLAC, right filename).

Fix

  • Serialize Spotify captures process-wide (_SPOTIFY_CAPTURE_LOCK): only one librespot device + playback + capture runs at a time. YouTube and torrent downloads are process/file isolated and remain concurrent. The lock is always released (capture is timeout-bounded with a finally that stops librespot), so it can't deadlock; queued downloads log how long they waited.
  • Unique librespot device name per capture (flacfetch-capture-<uuid>), resolved by that exact name — a stale/overlapping device can never be mistaken for this download's.
  • Fail loudly if the requested track never loads (the track-load check result was previously discarded), so we never save whatever else is playing.
  • Per-capture temp files + atomic publish: PCM/log/temp-FLAC carry the capture id and the final FLAC is published via os.replace(); a single outer finally always cleans them up (no orphan PCMs on disk).

Testing

  • New TestSpotifyConcurrencySerialization: proves two concurrent download() calls never overlap their capture, each uses a unique device name, a non-loading track fails, and a failed capture leaves no orphan temp files.
  • Full suite: 661 passed, ruff clean.

Reviewed locally with CodeRabbit (3 passes, all findings addressed).

@coderabbitai ignore

beveradb and others added 3 commits September 1, 2026 18:52
… (v0.29.0)

A Spotify account has a single active playback stream, but Spotify captures
ran in a thread pool with no serialization. Two overlapping /download-by-id
requests each started a librespot device (all named the constant
flacfetch-capture) and drove playback via the Web API on the same account.
The second start_playback() hijacked the account's one playback stream, so
the first download's capture pipe recorded the other track's audio while the
second starved and timed out -- silently producing input FLACs with the wrong
song (job 842948f4: source Maduk - One Last Picture, received Keeno - Shelter
from the Storm audio; both Spotify downloads started within 2 seconds).

- Serialize Spotify captures process-wide (_SPOTIFY_CAPTURE_LOCK); other
  sources (YouTube, torrents) still run concurrently.
- Unique librespot device name per capture (flacfetch-capture-<uuid>); resolve
  by that exact name so a stale/overlapping device can't be mistaken for ours.
- Fail loudly if the requested track never loads (the track-load result was
  previously discarded, so capture proceeded regardless).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Use full uuid for the librespot device name (was 8 hex chars / 32 bits;
  collision could match a stale device the change intends to avoid).
- Make intermediate capture files (PCM, librespot log) unique per download
  via capture_id, and publish the final FLAC via an atomic os.replace() from
  a per-download temp file. This closes a race where releasing the capture
  lock before conversion let a same-named concurrent download truncate the
  PCM mid-conversion or produce a half-written FLAC.
- Install shared test patches once in the main thread (ExitStack) instead of
  inside each worker thread, so patch setup/teardown can't interleave.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address second CodeRabbit pass:
- Always delete the per-capture PCM, librespot log, and temp FLAC via a single
  outer finally, so a failed capture (device not found, track not loaded,
  timeout) no longer orphans a tens-of-MB PCM on disk (capture files now carry
  a unique capture_id, so failures previously never got cleaned up).
- Pass the per-download device_name into _device_not_found_message so the
  diagnostic names the device we actually registered (flacfetch-capture-<uuid>)
  instead of the bare constant.
- Add regression test asserting a failed capture leaves no orphan temp files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@beveradb
beveradb merged commit cffb467 into main Sep 1, 2026
5 checks passed
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.63636% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
flacfetch/downloaders/spotify.py 83.63% 9 Missing ⚠️
Flag Coverage Δ
unittests 47.22% <83.63%> (-3.48%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
flacfetch/downloaders/spotify.py 67.87% <83.63%> (+24.15%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@beveradb
beveradb deleted the feat/sess-20260901-1844-spotify-concurrency-mixup branch September 2, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant