Skip to content

Prepare for v2.4.11 - #44

Merged
Stensel8 merged 15 commits into
devfrom
spotify-testing
Oct 7, 2026
Merged

Stensel8 merged 15 commits into
devfrom
spotify-testing

Conversation

@Stensel8

@Stensel8 Stensel8 commented Oct 7, 2026

Copy link
Copy Markdown
Owner

2.4.11

Spotify

  • Spotify links can be downloaded: tracks, albums, artists and playlists, as
    open.spotify.com links or spotify: URIs, and streamrip search spotify track|album|artist|playlist .... Spotify encrypts its own audio, so streamrip
    does what spotDL does: Spotify
    supplies the track list, tags (title, artists, album, track and disc numbers,
    date, copyright, ISRC) and cover, and the audio is the same recording on
    YouTube Music, found by ISRC and then by artist, title and length, and
    downloaded with yt-dlp. It is AAC (kept as it is) or MP3, never lossless, and a
    track YouTube Music only has as another version (live, remix, ...) is reported
    as not found. See the new Spotify section of the README, which also lists what
    Spotify allows an app.
  • You make a Spotify app of your own, and log in once. Since February 2026
    Spotify's API is only open to apps whose owner has Premium, with a client id
    and no secret: streamrip asks for the Client ID the first time, explains where
    to make the app (the redirect URI, ticking Web API), opens your browser to log
    in (OAuth with PKCE) and catches the answer on 127.0.0.1:9900, or lets you
    paste the address where the browser ends up, on a server without a browser. The
    login renews itself and is saved in the new [spotify] section.
  • Playlists are limited by Spotify to the ones you own or collaborate on. Other
    playlists, and podcasts and local files, are skipped, with a message that says
    why. Searching is capped at 50 results (Spotify gives ten per request).
  • The new [spotify] options: audio_format (m4a or mp3), audio_bitrate
    (for audio that has to be re-encoded), match_videos (accept an official music
    video when YouTube Music has no matching song) and redirect_uri.
  • A config from before this release has no [spotify] section: it is added from
    the template, and written the next time the config is saved. No config reset.
  • New dependencies: ytmusicapi and yt-dlp[default]. yt-dlp wants a
    JavaScript runtime (Deno or Node.js) for YouTube; a download that fails saying
    so is explained as such.
  • Credits are at the bottom of the README: the login and API handling and the
    matching are those of Music-Sync, the
    idea of metadata from Spotify and audio from YouTube Music is spotDL's, and
    MediaHarbor was read as a
    reference. Its decryption of Spotify's Widevine-protected audio is not used.

CSV lists

  • streamrip csv list.csv downloads the tracks of a CSV list, found by
    searching a source, which it asks for ("Where should I search and download them
    from?", with what each source gives) unless --source says. --fallback-source
    gives a second source for the tracks the first does not have. The columns of
    Music-Sync and of Exportify,
    TuneMyMusic and the like are understood, a file without a header row is read
    as artist,title, and commas, semicolons and tabs work as separators. The first
    five results of each search are scored on title, artist and length, as
    Music-Sync does, so a live version, a remix or another artist's cover is not
    taken for the track. The tracks go in a folder named after the file, as a
    playlist.

Lyrics

  • The lyrics of tracks whose source sent none are looked up on
    LRCLIB, a free, open database of lyrics that needs no
    account: Qobuz, SoundCloud and Spotify send none, and Deezer not for every
    track. It tells LRCLIB the artist, title and length of those tracks, so lyrics = false
    under [downloads] turns it off, and streamrip logs "No lyrics found for ..."
    for a track LRCLIB does not have. It asks with the length of the file
    just downloaded, as synced lyrics only fit one edit of a song, and a search
    result of another version, of another artist or of another length is not taken.
    Synced (LRC) lyrics are used, plain ones for MP3. LRCLIB being slow or down
    costs nothing: after three failures in a row it is left alone for the run, and
    a lyric that cannot be found never fails a track. Its session is a plain one of
    its own, never a source's, which holds that source's login.

Notices

  • The first time Spotify is used in a run, streamrip says its audio comes from
    YouTube Music and is lossy, and recommends moving the playlist first with
    Music-Sync (to Tidal, or to a CSV for
    streamrip csv), with a link.
  • The first time Deezer is used with lyrics on, streamrip says that Deezer does
    has no lyrics for every track, so the others are looked up on LRCLIB, and how to
    turn that off. The
    README and the comment in the config say so too. Deezer's answer that it has no lyrics for a
    track is no longer a warning for every track; a lyrics request that really
    fails still is.

Tests

  • find_ffmpeg is cached, and test_ffmpeg_utils left a fake path, or none, in
    the cache for the tests after it. They are forgotten after each test now.

claude and others added 11 commits October 6, 2026 21:28
find_ffmpeg is cached, and these tests left a fake path (or None) in the
cache for every test after them, so a later test that really needs ffmpeg
failed with "ffmpeg not found" when the whole suite ran, and passed alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
…sic (2.4.11)

Spotify encrypts its own audio, so Spotify is a metadata source here, the way
spotDL uses it: the Web API gives the track lists, tags and covers of tracks,
albums, artists and playlists (links, spotify: URIs, search); the audio is the
same recording on YouTube Music, found by ISRC and then by artist, title and
length, and downloaded with yt-dlp as AAC (kept as it is) or MP3.

Login follows the rules for apps in development mode since February 2026, as
Music-Sync handles them: an app of the user's own (its owner needs Premium),
OAuth with PKCE and a client id only, a loopback redirect on 127.0.0.1 or a
pasted address, tokens refreshed and saved in the new [spotify] section. The
prompter explains how to make the app. Playlists are limited to the user's
own and shared ones, and search to ten results per request (50 in total).

Matching is adapted from Music-Sync's matching.py, with spotDL's ISRC-first
lookup, a length check, and a fallback to official videos (match_videos).

A config from before this release has no [spotify] section; sections the file
lacks are now added from the template instead of failing the load.

Bumps the version to 2.4.11 and adds ytmusicapi and yt-dlp[default].

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
The Spotify section explains what it does and does not give (audio from
YouTube Music, not lossless), the Premium rule, how to make the app step by
step, and what Spotify lets such an app read. The credits are at the very
bottom: Music-Sync, spotDL and MediaHarbor, and that Spotify's own audio is
not decrypted.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
…icks

`streamrip csv list.csv` reads a list of tracks and asks "Where should I search
and download them from?" (qobuz, tidal, deezer, spotify or soundcloud, with what
each gives); --source answers it for scripts, and without a terminal and without
--source it fails instead of waiting. --fallback-source is a second source for
the tracks the first does not have.

The columns of Music-Sync and of Exportify, TuneMyMusic and the like are
understood, a file without a header row is read as artist,title, and commas,
semicolons and tabs separate. Each track is searched (the title without its
brackets first), and the first five results are scored on title, artist and
length with the matching of Music-Sync, so a live version, a remix or another
artist's cover is not taken for the track. The tracks go in a folder named after
the file, as a playlist, a track listed twice only once.

feat(notices): say once per run what to know about Spotify and Deezer

Spotify: the audio comes from YouTube Music and is lossy; move the playlist first
with Music-Sync (to Tidal, or to a CSV for `streamrip csv`), with its link.
Deezer: it does not send lyrics. Deezer's error answer to the lyrics request is
no longer a warning for every track; a request that really fails still is.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
The README gets a CSV lists section (the format, how a match is chosen, where the
tracks go), the advice to move a Spotify playlist first with Music-Sync, and the
fact that Deezer does not send lyrics; the lyrics comment in the config says so
too. The 2.4.11 changelog has the three.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
…LRCLIB

Opt-in: `lyrics_fallback = true` under [downloads] (off by default, as it tells
LRCLIB the artist, title and length of those tracks). Deezer sends no lyrics, and
for Qobuz, SoundCloud and Spotify there are none to ask for.

Just before a track is tagged, LRCLIB is asked by the main artist, the title and
the length of the file just downloaded (its /get takes a length within two
seconds, so another edit is not taken for the song). When it has no such record,
its search results are scored like a YouTube Music result, with a limit of three
seconds on the length: synced lyrics only fit one edit. Synced (LRC) lyrics go in
M4A and FLAC, plain ones in an MP3 (also when converting to MP3). Lyrics a source
sent, `lyrics = false` and `exclude = ["lyrics"]` are respected.

The session is a plain one of its own, never a source's, as those hold the
source's login. Results are remembered per track, three failures in a row leave
LRCLIB alone for the run, and a lookup that fails never fails a track.

The Deezer notice now says what to turn on, or that the lyrics are looked up.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
A Lyrics section (what each source sends, what the fallback does and does not,
that it is off by default and why), the Deezer note pointing to it, and LRCLIB in
the acknowledgements.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
@Stensel8 Stensel8 self-assigned this Oct 7, 2026
Copilot AI balanced review requested due to automatic review settings October 7, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

CSV parsing, metadata completeness, error handling, and cancellation concurrency have unresolved correctness issues.

Review effort: Balanced
Findings: 5 Medium severity · 3 Low severity

Open (8)
What changed in this PR

Prepares v2.4.11 with Spotify downloads, CSV playlist imports, and LRCLIB lyric lookup.

Changes:

  • Adds Spotify OAuth, metadata, matching, and yt-dlp audio downloads.
  • Adds CSV-based playlist resolution with fallback sources.
  • Adds LRCLIB lyrics, source notices, documentation, and tests.
File Description
tests/​test_ytmusic_match.py Tests YouTube Music matching.
tests/​test_ytdlp_downloadable.py Tests yt-dlp downloading and encoding.
tests/​test_spotify_metadata.py Tests Spotify metadata and URLs.
tests/​test_spotify_login.py Tests OAuth callback handling.
tests/​test_spotify_config.py Tests Spotify configuration and prompting.
tests/​test_spotify_client.py Tests Spotify API integration.
tests/​test_reauth_prompt.py Updates reauthentication fixture state.
tests/​test_notices.py Tests per-run source notices.
tests/​test_lyrics.py Tests LRCLIB lookup and tagging.
tests/​test_ffmpeg_utils.py Clears cached ffmpeg results.
tests/​test_ffmpeg_preflight.py Covers CSV ffmpeg preflight.
tests/​test_error_handling.py Mocks the new Spotify client.
tests/​test_deezer_client.py Tests Deezer lyric errors.
tests/​test_csv_playlist.py Tests CSV parsing and matching.
tests/​test_csv_cli.py Tests the CSV command.
tests/​test_config.py Covers Spotify and lyric defaults.
tests/​test_cli.py Updates CLI help and commands.
tests/​test_audio_match.py Tests shared recording matching.
streamrip/​rip/​spotify_login.py Implements Spotify OAuth callback capture.
streamrip/​rip/​prompter.py Adds interactive Spotify login.
streamrip/​rip/​parse_url.py Recognizes Spotify links and URIs.
streamrip/​rip/​notices.py Adds Spotify and Deezer notices.
streamrip/​rip/​main.py Integrates Spotify, CSV, lyrics, and notices.
streamrip/​rip/​cli.py Adds the csv command.
streamrip/​metadata/​track.py Maps Spotify track metadata.
streamrip/​metadata/​search_results.py Supports Spotify search pages.
streamrip/​metadata/​playlist.py Maps Spotify playlists.
streamrip/​metadata/​covers.py Maps Spotify cover sizes.
streamrip/​metadata/​artist.py Maps Spotify artist releases.
streamrip/​metadata/​album.py Maps Spotify album metadata.
streamrip/​media/​track.py Looks up missing lyrics.
streamrip/​media/​csv_playlist.py Implements CSV playlist resolution.
streamrip/​media/​__init__.py Exports CSV playlist support.
streamrip/​lyrics.py Implements LRCLIB integration.
streamrip/​db.py Adds Spotify download keys.
streamrip/​config.toml Documents Spotify and lyrics options.
streamrip/​config.py Adds Spotify config and section migration.
streamrip/​client/​ytmusic.py Finds matching YouTube Music audio.
streamrip/​client/​spotify.py Implements the Spotify client.
streamrip/​client/​downloadable.py Adds yt-dlp-backed downloads.
streamrip/​client/​deezer.py Adjusts lyric error handling.
streamrip/​client/​audio_match.py Adds shared track scoring.
streamrip/​client/​__init__.py Exports SpotifyClient.
streamrip/​__init__.py Bumps the package version.
README.md Documents new functionality.
pyproject.toml Bumps version and adds dependencies.
CHANGELOG.md Adds v2.4.11 release notes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread streamrip/client/deezer.py Outdated
Comment thread streamrip/client/downloadable.py
Comment thread streamrip/client/spotify.py
Comment thread streamrip/lyrics.py Outdated
Comment thread streamrip/media/csv_playlist.py Outdated
Comment thread CHANGELOG.md Outdated
Comment thread README.md Outdated
Comment thread streamrip/rip/notices.py Outdated
claude and others added 4 commits October 7, 2026 11:28
… csv tests under log_cli

Click named the group after its function, so usage lines and errors said
`rip csv [OPTIONS] PATH` and `Try 'rip csv --help' for help.`. The group is
called streamrip now; a test covers the help, a command's help, a usage
error and an unknown command.

The csv tests read Click's errors from result.output. With log_cli on (the
repo's default), pytest's live log puts its own streams back in the middle of
CliRunner.invoke, so the errors were in pytest's stderr capture instead, and
CI failed six tests that passed locally with -o log_cli=false. They (and
tests/test_cli.py's _output helper) read that capture too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
CSV: the separator of a file is the one that cuts its first rows into the same
number of cells. Counting characters in the first line took a comma in a name
("Earth, Wind & Fire";September) for the separator: every row was dropped, or
a row was read as another track.

Lyrics: LRCLIB is not asked again once it is left alone, also by lookups that
were already waiting for a slot. A lookup that got no answer is no longer
logged as "No lyrics found" (LyricsUnavailableError); it is said once, as a
warning, when LRCLIB is left alone. Deezer: only a DATA_ERROR answer to the
lyrics request means "no lyrics"; any other GW error is a warning again.

yt-dlp: a cancelled download waits for its thread before the temporary folder
is removed. Bun is no longer enabled as a JavaScript runtime, as yt-dlp has no
way to run it without access to the computer (Deno and Node it does).

Spotify: a playlist says how many local files, episodes and gone tracks it
skipped. The changelog no longer promises the copyright for a single track or a
playlist track: their album is a simplified one, without it.

Texts: "Deezer does not have lyrics for every track" (README, CHANGELOG and the
notice), and the typo "does has" in the changelog.

Tests: no test asks the real lrclib.net any more, or leaves a session open.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
…celled one cannot wait for ever

A download that was still waiting for a free thread of the shared pool (a small
or busy machine) and was then cancelled twice, as asyncio.run does when Ctrl-C
is followed by the force-quit, never ended: its job was cancelled before it
began, so the flag it was to set was never set, and the wait for that flag went
on, swallowing every cancel.

Each download now has an executor of one thread, and what is waited for is the
job itself: done when it finished, and also when it never began. That drops the
extra task, its done callback and the flag. The tests of the cancel stay as they
are, and a new one covers a job that never began.

Claude-Session: https://claude.ai/code/session_015uHHKJwGzunRmkVVukaGyL
@Stensel8
Stensel8 merged commit 6c9c678 into dev Oct 7, 2026
13 checks passed
@Stensel8
Stensel8 deleted the spotify-testing branch October 7, 2026 14:55
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.

3 participants