Repository navigation
Prepare for v2.4.11 - #44
Merged
Merged
Conversation
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
…pp in development mode gets
…IB, and say when there are none
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
CSV parsing, metadata completeness, error handling, and cancellation concurrency have unresolved correctness issues.
Review effort: Balanced
Findings: 5
Open (8)
Only suppress recognized no-lyrics API responses · New Await worker termination before deleting the temporary directory · New Fetch full album metadata for track and playlist downloads · New Recheck the circuit breaker before each lookup · New Use quote-aware delimiter detection · New Correct “does has” to “does not have” · New Clarify that Deezer lacks lyrics for some tracks · New Clarify wording about tracks without lyrics · New
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.
… 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
…ncelled, so its folder is gone for good
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


2.4.11
Spotify
open.spotify.comlinks orspotify:URIs, andstreamrip search spotify track|album|artist|playlist .... Spotify encrypts its own audio, so streamripdoes 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.
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 youpaste 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, and podcasts and local files, are skipped, with a message that says
why. Searching is capped at 50 results (Spotify gives ten per request).
[spotify]options:audio_format(m4aormp3),audio_bitrate(for audio that has to be re-encoded),
match_videos(accept an official musicvideo when YouTube Music has no matching song) and
redirect_uri.[spotify]section: it is added fromthe template, and written the next time the config is saved. No
config reset.ytmusicapiandyt-dlp[default]. yt-dlp wants aJavaScript runtime (Deno or Node.js) for YouTube; a download that fails saying
so is explained as such.
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.csvdownloads the tracks of a CSV list, found bysearching a source, which it asks for ("Where should I search and download them
from?", with what each source gives) unless
--sourcesays.--fallback-sourcegives 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 firstfive 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
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 = falseunder
[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
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.lyricson, streamrip says that Deezer doeshas 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_ffmpegis cached, andtest_ffmpeg_utilsleft a fake path, or none, inthe cache for the tests after it. They are forgotten after each test now.