All notable changes in this fork of nathom/streamrip. Numbers can refer to upstream issues and pull requests.
Small bugs and slow spots, found by testing harder and pushing streamrip to its limits.
- fix: a failed conversion or download never touches a file you already had
- fix: whole discographies: Tidal artists past 100 albums, Qobuz past 500
- fix: one bad url no longer stops the rest, and
repair, the artist filters, tags, covers and logins handle their edge cases - feat: rate limits per service in the config, Tidal's measured ones by default; Deezer keeps to them too
- perf: Tidal hi-res needs about 65 fewer requests a track, and tagging no longer holds up other downloads
- chore: dependencies updated, the tests in 20 files instead of 64, and CI
tests real accounts with a
STREAMRIP_CONFIGsecret
- feat: a container image of 50 MiB instead of 190; it needs Docker 23 or newer, or Podman
- feat: more tags: label, barcode, release type, BPM, key, ReplayGain, lyricists, every Qobuz composer and explicit, named as MusicBrainz Picard writes them
- feat: your liked tracks and albums on Tidal, and your liked albums on Deezer
- feat: Deezer's search shows release date, length, genre and label, and covers show in full color
- chore: four dependencies fewer: aiofiles, m3u8, pathvalidate and deezer-py. Deezer resolves albums about twice as fast and only talks https
- fix: Tidal and Qobuz tracks failed after downloading with
'NoneType' object has no attribute 'clear'since 2.6.7 (#91, reported by @Vaela-V and @calavikevin) - feat: the container image is also on
opencommit.eu/stensel8/streamrip, and arm64 is built on arm64 hardware - feat: opencommit.eu releases get the wheel too
- fix: the container image gets Debian's security fixes when it is built, not only when its base image moves (zlib, CVE-2026-85091)
- docs: opencommit.eu is the primary platform, and the links go there
- docs:
mediumis no longer called "unproven" -- changes in wording/docs, based on tests the maintainer did against Tidal's own API:medium's ~150 tracks an hour is just as tested asslow's ~80, only withoutslow's own 18-hour clean run - docs:
--pace --helpis far shorter, pointing at[downloads] pacingin config.toml for the full story instead of repeating it - feat: the update check now also tries
opencommit.eu when GitHub
itself doesn't answer, and suggests
uv tool installon the released wheel instead of a git source install where that is the install in use
- feat:
[downloads] create_album_folder = falseputs tracks directly in the downloads folder instead of a per-album subfolder (based on #90 by @Xenxia) - feat:
[downloads] save_lrcalso writes lyrics to an.lrcfile - fix: a fresh config downloads to the user's real Music folder, not
~/StreamripDownloads - fix: a FLAC's embedded cover now carries its real width/height/depth
- fix: retagging no longer leaves an earlier pass's tags behind (e.g. ffmpeg's "encoder" comment)
- A track's download url is refreshed if it has gone stale (queued behind
max_connections, or across a retry) instead of failing with HTTP 403. --pacegainsmedium: a faster, unproven ~150 tracks an hour for a session, betweenfast(no waiting, not for hours on end) and the provenslow(~80 an hour).autostill only ever resolves tofastorslow.
- Tracks are paced to protect your account.
--pace auto(the default) rips a small job at full speed and a big one at a steady pace of about 80 tracks an hour;--pace fastand--pace slowchoose one.pacingin[downloads]sets it for good. - Under the progress bars, each account shows its load: tracks this hour out of 175, the pace, the next track's turn and a ban risk. Tidal starts refusing from about 200 tracks an hour; at full speed streamrip warns past 175.
- After a rate limit (HTTP 429), requests are spread out further and each one is reported. At 10 within an hour, counted across runs, streamrip stops and advises a break of 3 hours.
- While tracks are paced, the time left on the Done bar follows the pace.
- Options such as
--paceand--qualityalso work after the command. - The progress display no longer leaves its title line behind at every redraw on a terminal that wraps at its last column (PR #89, by @statelycurmudgeon).
- A Tidal login that lapses mid-run is renewed. One that Tidal refuses, such as a suspended account, stops the run with the reason.
- SoundCloud downloads work again, as 160 kbps AAC (
.m4a, labelled[AAC]). - A last.fm playlist's tracks are matched on title and artist, like a CSV list's.
- A failed login or a stop ends in one red line and exit code 1, not a traceback.
- Stopping streamrip also stops its ffmpeg and leaves no half-written file.
- Counts read "1 track", "2 tracks", and a search that finds nothing no longer reports "0 tracks downloaded".
streamrip config pathprints only the path.checks/run.pyruns every test by hand, including the login tests CI skips, and checks real accounts.- streamrip no longer claims one fixed user agent forever. Requests present a current desktop Chrome, complete with the low-entropy Client Hints a real one sends beside it (Sec-CH-UA, -Mobile, -Platform, Accept-Language) and, on API calls, the fetch headers of a web player; media downloads stick to the identity itself. Every 45 to 110 minutes it becomes a slightly different Chrome (another version, platform or language), so a long run shows a service no single fingerprint to flag as a bot. The brand list is computed the way Chromium computes it from the version number, so the hints can never contradict the user agent. Deezer's API calls follow along, each keeping the language of the account it belongs to. LRCLIB is still told the truth: it asks for a user agent that says what is asking.
- A hi-res track downloaded on its own into its album's folder
(
add_singles_to_folder) is named[24B-96kHz]there, not[16B-44.1kHz].
- The browser's messages ("Opening in existing browser session.", zygote errors) are no longer shown in the terminal when a login page opens. (#81)
streamrip now runs on Python 3.15 only, and everything written for older Python versions, older configs and older databases is gone. Read Upgrading first.
- Python 3.15 is required (2.5.1 also took 3.14). The installers fetch it with uv.
- A config from before 2.4.11 no longer loads, unless saved since. Every
option has to be in the file; streamrip says so and
streamrip config resetwrites a new one (which also clears your logins). The sections and options that earlier releases added (Spotify, the second Tidal login, lyrics,prefer_explicit) are no longer filled in for an old config. - Rows of the downloads database written before they carried their source
(a bare
123instead oftidal_123) no longer count as downloaded, so those tracks download once more. The rebuilding of an old failed-downloads table (one unique on its id alone) is gone too. pip install .no longer includes Playwright. It is thebrowserextra now:pip install ".[browser]". The installers include it. Thesslextra is gone, as certifi is always installed.
- A container image for Podman and Docker, built from a
Containerfileand published to ghcr.io for amd64 and arm64 on every release. It has ffmpeg and Deno built in and keeps its config, databases and downloads in/data:podman run --rm -it -v ./streamrip:/data ghcr.io/stensel8/streamrip url <link>. Under rootless Podman the files are yours without further flags; Docker needs--user "$(id -u):$(id -g)". - The image is about 390 MiB unpacked and 150 MiB to pull. It leaves out Playwright, which needs a screen, and pip, which only installs and carries older copies of certifi and urllib3. It needs no compiler to build.
- It does not check for updates, as a container is updated by pulling a newer
image and the check cost about half a second of every run. That is the new
STREAMRIP_NO_UPDATE_CHECKenvironment variable, which turns the check off anywhere (any value but empty,0,falseorno), next tono_update_checkin the config. - The image is built and scanned for known vulnerabilities with Trivy on every change to it and every Monday, and the check fails on a fixable HIGH or CRITICAL finding. Trivy's supply chain was compromised in March 2026, so the action is pinned to the commit of an immutable release.
- SIGTERM, what
podman stop,docker stop, systemd andkillsend, now stops streamrip like Ctrl-C does: the running command is cancelled and cleaned up. It exits with 143, as a process ended by SIGTERM does. As the first process of a container it was ignored before, sopodman stopwaited out its timeout and then killed it. This also works at a prompt and in commands that need no event loop. Ctrl-C behaves as before, and a second one forces it.
- The isolated browser window of the Qobuz and Deezer logins is only offered where it can work: with Playwright installed and, on Linux, a screen to open it on. In a container or over SSH, where the browser runs elsewhere and can't reach streamrip, Qobuz and Deezer go straight to entering the login by hand, and Spotify defaults to pasting the address.
- Qobuz and Deezer print the address of the login page before opening it. Nothing showed it before, so without a browser to open it there was no telling where to go.
streamrip --versionstarts about six times faster (measured with the update check off). The package, the CLI and the clients load aiohttp, Pillow, Textual and the other heavy libraries only when a command needs them (PEP 810), and--helpand--versionno longer load asyncio.- Tidal albums and playlists take a third fewer requests: a track's metadata comes from the album's or playlist's track list instead of a request of its own. At the default of 35 requests a minute that is about 17 tracks a minute instead of 12.
- Downloads start sooner with
prefer_explicit, on Tidal right away. Choosing between a clean and an explicit copy only needs the tracks' metadata, so only that is looked up for every track first (on Tidal without a request), and each track's stream is fetched as it downloads. Before, every track was fully resolved first, which on Tidal looked like minutes of nothing happening. An 11-track Tidal album takes about 37 seconds of requests instead of 56. - A row counts the tracks being looked up, with the time left. The bar of the
tracks finished is called
Doneinstead ofOverall, as tracks are looked up while others download. - Lyrics are fetched after a track has downloaded, not before.
- Deezer's lyrics are looked up on LRCLIB, like Qobuz's. Deezer's own lyrics request answered "no data" for nearly every track, and the notice about that on first use is gone.
- Native Python 3.15 throughout: paths are
pathlib.Pathinstead ofos.pathstrings, generics use PEP 695,asyncio.timeoutandcreate_taskreplacewait_forandensure_future, constants that are only read arefrozendict, and thefrom __future__imports and quoted annotations are gone. Imports that only annotations need are lazy (PEP 810) instead of behindTYPE_CHECKING, and comprehensions unpack (PEP 798). Ruff now enforces the modern forms (UP,PTH,B,C4,FURB,PERF,PIE,RET,SIM,TC).
- The project is managed with uv instead of Poetry:
uv.lockreplacespoetry.lock, the dev tools are a[dependency-groups]group, anduv_buildbuilds the package. CI, releases, Renovate and Dependabot use it too. - The installers run
uv sync, so they install the versions ofuv.lockand remake a.venvthat is not on Python 3.15 by themselves. They include ffmpeg now (theffmpegextra), so it no longer has to be installed separately. The container image is built fromuv.lockas well. - The Nix flake installs the same wheels through uv2nix: it compiles nothing and no longer depends on which Python libraries nixpkgs has, so the exceptions for Pillow, certifi and yt-dlp are gone with it.
click-help-colorsis gone: it needsclick.MultiCommand, which Click 9 removes.--helplooks the same, with the same colors andNO_COLOR.yt-dlp[default]is replaced by exact pins on yt-dlp 2026.10.21 and yt-dlp-ejs 0.8.0 (yt-dlp only accepts the solver script of the version it was released with), plus websockets and certifi. The extra pulled in brotli, which has no wheel for Python 3.15: every dependency now installs from a wheel on Linux (x86 and ARM), Windows and macOS, so no compiler is needed. yt-dlp only uses brotli to unpack some pages; without it YouTube pages come gzip-compressed, about 190 KB more each.- Renovate also pins and updates the Containerfile's base images, pins the development tools to exact versions, and no longer merges major updates by itself (the rule said to wait for a human but merged them).
- Pillow 13, a security update (#68); textual-image 0.14.2 (#66); certifi 2026.10.19 (#71); and a refresh of the lock file (#65).
- The README is shorter: the three ways to run streamrip (Python, Podman or
Docker, Nix) with their syntax, and one example per command, for Linkin Park.
Spotify, logging in, lyrics, CSV lists, the container and Nix moved to
docs/. - The tests are plain pytest with
async def,parametrizeandtmp_path, repeated cases are merged, and the tests that needed a Qobuz or Deezer account are gone. They no longer write into the real~/.config/streamrip. - Removed what nothing used:
.github/stale.ymlandtests/fixtures/.
- Joining the HLS segments of a long SoundCloud track no longer starts all its ffmpeg processes at once. The segments are joined 128 at a time, with a process for each batch, and all batches started together: a mix of thousands of segments made dozens of processes, which can run into process or memory limits. At most four run at a time now. (#32)
- Cancelling a SoundCloud download (Ctrl-C), a failing ffmpeg process, or one that cannot start no longer leaves anything behind. The processes still running are killed, and the intermediate files they were writing are removed from the temp folder. (#32)
- The lock file moves to Playwright 1.64.0, pycryptodomex 3.24.1 and tomlkit
0.15.2, and also pyee 14.0.0 (which Playwright uses), platformdirs 4.13.5 and
certifi 2026.10.18.
pyproject.tomlallows the same versions as before: an install from the repository already got the newest ones, the lock file only pins what Poetry and CI use. - The development tools move to Ruff 0.17.1, pytest-mock 3.17.0 and pluggy
1.7.0, and CI to
cachix/install-nix-action31.11.2 and CodeQL 4.39.0.
Thanks to @berettavexee for the unified progress bar below, and to @Multipixelone for the Nix flake below.
- One overall progress bar, counting tracks, for every download. An artist
or label download used to get a top bar counting albums (
3/37 albums), and a single album or playlist only the per-track rows of what was downloading right then, so on a 59-track playlist you could not tell whether you were at track 5 or 50. From the user's side those are the same thing: a lot of tracks, and you want to see the overall progress. The album-count bar, the resolve spinner ("Obtaining album info…") and the per-source bars are gone; in their place is one bar (Overall,12/15 tracks) that album, playlist and artist downloads all share: an artist's total grows as each of its albums starts, because its tracks are only known then. A track counts on it whatever its outcome, resolved or not, so the bar never stalls on a failure. The bar is reset between runs, and its running total is kept by streamrip, not read back from Rich's task list, which is reindexed when a task is removed. (#47)
- A Nix flake (
nix build,nix run,nix develop,nix flake check) for x86_64-linux, with nixpkgs pinned inflake.lock, on Python 3.14: the package with an explicit dependency list kept in sync withpyproject.toml, a development shell with the same dependencies plus Poetry and Ruff, and FFmpeg, Deno and Chromium onPATH. The package runs the non-browser test suite and offline CLI smoke checks (--version,--help, loading a config). Renovate keeps the nixpkgs pin up to date, and CI runsnix flake checkon every change. (#46)
- Spotify links can be downloaded: tracks, albums, artists and playlists, as
open.spotify.comlinks orspotify:URIs, andstreamrip 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, ISRC, and the copyright of an album: Spotify leaves it out of the album it gives with a single track or a playlist track) 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(m4aormp3),audio_bitrate(for audio that has to be re-encoded),match_videos(accept an official music video when YouTube Music has no matching song) andredirect_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. Noconfig reset. - New dependencies:
ytmusicapiandyt-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.
streamrip csv list.csvdownloads 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--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 asartist,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.
- 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 = 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.
- 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
lyricson, streamrip says that Deezer does not have 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.
find_ffmpegis cached, andtest_ffmpeg_utilsleft a fake path, or none, in the cache for the tests after it. They are forgotten after each test now.
- An album folder can no longer end up outside the downloads folder. Names from
a streaming service decided where it went: a field that came out empty in a
folder format like
{albumartist}/{title}gave/Title, a folder in the root of the filesystem, and an artist or uploader called..(SoundCloud names are free text) gave../Title. Empty parts of a folder path are dropped now,.and..become underscores, both kinds of slash separate folders, and a folder that is not below the downloads folder is refused.
- SoundCloud tracks are skipped when already downloaded. They were downloaded again on every run.
- The downloads database keeps ids per source (
tidal_2430924980, not2430924980). A track was skipped as downloaded as soon as one of another source with the same id had been, which gets likelier the more you download. Entries from before hold the bare id, cannot be told apart and still count for every source, so nothing is downloaded again after updating;streamrip database clear downloadsforgets them.streamrip database browse downloadsshows the source. - The failed downloads database is unique per source too. A failure of one
source kept another's with the same id out of it, and
streamrip repairremoved both. An existing database is converted on first use.
- Saving the config is atomic. A crash, Ctrl-C or full disk could leave an empty file with your logins gone. A symlinked config stays a symlink.
no_update_check = trueunder[cli]turns the update check off. It is commented out in the config by default.
- A config that does not load ends the command with exit code 1, not 0.
- The update notice says its command needs git.
- The browser login tests are skipped without a Chrome-family browser, and
fail on CI, instead of waiting for an answer on stdin.
pytest -m "not real_browser"leaves them out.
Thanks to @berettavexee for the resume check, the SoundCloud fallback, the Deezer playlist fix and the traceback fix below.
- One menu for every OS. The separate Linux/macOS and Windows menus
(
simple-term-menu,pick,windows-curses) are replaced by a single Textual menu that works the same everywhere. Filter the results with/, mark several withSPACE;ENTERdownloads the marked results and the highlighted one. The mouse works too. - The selected result's cover shows beside its details. Terminals with SIXEL or
Kitty graphics support (Konsole, foot and Windows Terminal, for example) get
the image, the others colored sextant blocks, which are less sharp. Detection
is automatic, and the choice is logged with
-v. Covers are fetched on demand with a small cache, and moving on cancels the pending request. A source's larger artwork is tried first, its smaller sizes are the fallbacks.
- A resumed download is checked against the server's
Content-Range. A server answering from another offset, a remainder that does not add up to the announced total, a close-delimited answer cut short, or a 416 taken as "already complete" whatever the partial file's size, used to end as a corrupt file. Now the partial file is removed and the track fails withIncompleteDownloadError, so the next attempt starts from scratch instead of resuming onto the same mismatch (PR #28, by @berettavexee).
- A geoblocked or delisted playlist track, which Deezer serves from another release, is tagged from the track that is served. It kept the requested track's metadata before, so the file got the album, title and cover of a release it does not come from. For such releases Deezer has no cover, only a grey placeholder, and that is what ended up embedded. The cover is fetched after the track is resolved, so only the right one is downloaded (PR #36, by @berettavexee).
- A track marked downloadable no longer fails outright. Its original now comes
back as a 401 with an empty body when asked anonymously, which crashed with a
ContentTypeError. The track's MP3 stream is used instead, and a track without one is reported as not streamable instead of failing an assertion (PR #34, by @berettavexee).
-vtracebacks no longer print local variables. They included the Deezerarlafter a failed login and every config, credentials and all, and a traceback is exactly what people paste into an issue. The stack itself is unchanged (PR #35, by @berettavexee).
- streamrip supports Python 3.14 and 3.15, not newer:
requires-pythonis now>=3.14,<3.16. CI runs the tests and the install check on both.
Thanks to @berettavexee for the Deezer quality fix and the FLAC cover fix below.
- Logging in no longer means digging the
arlcookie out of DevTools. streamrip offers a choice: an isolated browser window where you log in yourself and streamrip keeps only thearlcookie and never sees your password, or entering it by hand, with the steps printed. Entering it by hand is the default, because the browser option may have to download a browser first. Deezer has no sign-in for outside apps, and its email and password login is behind a captcha. - The quality comes from asking Deezer, not from
FILESIZE_*. Those fields are often 0 for a format Deezer does serve, which made streamrip download such tracks as MP3 320, or skip them ("Missing download info"). Each quality is requested from the wanted one down and the first that returns a URL is used; the file extension comes from the CDN URL. A quality Deezer lists a size for but gives no URL for (a rate limit, say) fails the track instead of silently falling back to a lower one (PR #26, by @berettavexee, with a follow-up for the rate-limit case). - Album folders and labels name the quality the tracks come in. They always said
[FLAC] [16B-44.1kHz], so-q 1gave a FLAC-named folder full of MP3s. It is now your configured quality within what your subscription allows, lowered to the best quality every track of the album has: one track without FLAC makes it an MP3 album. Such a folder can still hold a FLAC, since every track is asked for the best quality it has. - Lossy albums leave the empty
[UnknownB-UnknownkHz]out of the folder name. This also applies to Tidal AAC and SoundCloud.
- Qobuz writes several track artists as separate
ARTISTvalues, main artists first and then the featured ones, in the same order as Tidal. Qobuz lists credits in no fixed order, so the clean and the explicit edition of a track used to tag different artists. - Several album artists are written as separate
ALBUMARTISTvalues for every source, instead of oneA, B, Cstring. Libraries that split multi-valued tags (Music Assistant reads them through ffprobe) showed a release credited to several artists as one extra artist. Folder names still use the joined string. - Tagging a FLAC a second time after conversion (FLAC to FLAC) no longer embeds the cover twice (PR #27, by @berettavexee).
- Singles and playlist tracks are tagged with their album's own metadata. A track response only names its album, so a feature credit ended up in the album artist, the track count was missing and the stream date stood in for the release date. The album is fetched once per album; if that request fails, the old behaviour is the fallback.
- The device login waits for the link's real lifetime (5 minutes, not the "10 minutes" it announced), says so when the link expired, and a real rejection includes Tidal's own description.
- Small playlists, which come with every track's metadata up front, no longer fail every track with "not enough values to unpack".
- The preview in
streamrip searchshows the same details for every source, next to the cover: a track's album, number, release date, length, genre, label, composer, BPM, key and quality, and likewise for albums, artists and playlists. Covers are drawn with quadrant blocks, twice the detail across of half blocks.
- Qobuz and Deezer log in through the same browser helper: the same window, the same 5-minute wait (Qobuz's was 2 minutes) and the same "Credentials saved to config file" message.
- A download whose size is unknown gets a pulsing bar. A total of 0 made it look finished: a full bar without a spinner.
[misc] check_for_updatesis gone: the update check runs before every command by design and never read it. Config files with a[misc]section still load, and so do ones without.
- Cleanups:
Downloadableis a plain base class, the media types are one table, the redundant album semaphore is gone, playlists hold track ids instead of full metadata, and the progress bars, Tidal client and SoundCloud helpers share their code. The live Qobuz tests run again (setQOBUZ_USER_IDandQOBUZ_AUTH_TOKEN). streamrip.metadatano longer exportsAlbumSummary,ArtistSummary,TrackSummary,PlaylistSummaryandLabelSummary; oneSummaryreplaces them.
- Items download one after another: a selected artist's (or label's) whole discography finishes before the next one starts, one album at a time. This also helps to keep rate-limits under control. Several artists used to run at once, which mixed their tracks together on screen and ran into the rate limit until everything stalled.
- A progress bar per artist or label shows how far through its discography
the run is (
32% • 12/37 albums), and the screen is cleared when the next artist starts. Track rows show whose track they are. - Every album gets a
Finished <artist> - <album> [quality]line when done. Albums of three tracks or fewer (singles, EPs) used to get no line at all. - The summary at the end counts tracks, downloaded, already downloaded and
failed, instead of "1 item(s) downloaded" for a whole discography, and
points to
streamrip repairwhen something failed. The empty "Downloading" line no longer stays behind after the run.
- Artist discographies leave out releases by other artists now.
- A release listed twice (a clean and an explicit copy, or two quality tiers) downloads once, as Tidal already did.
- Email/password login is gone; Qobuz stopped accepting it long ago. In the
config,
use_auth_token,email_or_useridandpassword_or_tokenare replaced byuser_idandauth_token. Existing users should delete their config file.
requests_per_minutespaces requests evenly over time, instead of sending them all at once. This helps to avoid hitting rate limits.- Qobuz and SoundCloud requests now retry connection errors, timeouts, 5xx
and rate limits (429) the way Tidal's already did. A single timeout used
to cost a Qobuz album or track. SoundCloud now honours
requests_per_minuteat all. - Cover art downloads retry network errors, so a brief DNS or connection failure no longer leaves an album without artwork. This is in line with the retry behavior for the track and album downloads that we already did.
- Every request uses the same, current user agent (Chrome 155), including Deezer's API calls, which still sent Chrome 79's before this change.
- Update notices show which environment will be upgraded and use its active
Python (or
uv) for the suggested command. A failed update check no longer prevents the CLI from starting. - Install scripts install from the local source archive and report the full paths to the environment and executable. The README clarifies which release archive includes those scripts.
- Qobuz login now offers a choice: an isolated browser that logs in and captures the token automatically, pasting a short script into your own browser's console, or entering it by hand. Falls back to manual entry if the automatic options time out.
- Qobuz search accepts the result limit loaded from the TOML config, fixing
the crash when searching without
--num-results. - Tidal hi-res (DASH) downloads no longer leak MP4 container metadata
(
major_brand, ffmpeg'sencoderstamp, ...) into the FLAC's tags. - Removed the config version/migration system: an incompatible config now
just says to run
streamrip config reset, instead of being silently rewritten through a growing pile of per-option migration cases. max_connections(tracks downloaded at once) defaults to 5, down from 6.- A fully successful download now reports completion too, not just failures.
- The update check now runs, with a spinner, before every download command
(
url,file,search,lastfm,id) instead of onlyurl. The suggested upgrade command pins the detected release instead of installing whateverdevcurrently has.
- ffmpeg is required and checked before any login. Without it streamrip stops with install instructions and exit status 1, instead of failing every hi-res Tidal track one by one.
Security hardening.
- Config files and the app directory are owner-only (
0600/0700). - Playlist folders stay inside the download folder, and artwork cleanup no longer deletes existing files.
- Invalid disc numbers are rejected before any file is written.
- Qobuz search and Last.fm playlist pagination are bounded (Last.fm: max 10,000 tracks) and fetched one page at a time.
- Qobuz artist URLs are fetched over HTTPS without account credentials. Redirects are now an error: use a URL that contains the artist id.
- Albums and playlists download one at a time, with a spinner and status
line while a large one is still resolving. Track progress no longer mixes
two releases together, each track/album shows its format and quality
(
[FLAC 24B-96kHz]), and the doubled-up, corrupted terminal output during a long run is fixed. - Tidal: an album it lists more than once under one artist -- a clean/explicit pair, two quality tiers, or a "(Deluxe)"/"[Deluxe]" spelling of the same edition -- downloads only its best copy instead of every copy.
- Tidal: requests held back together by a rate limit no longer all retry in the same instant and immediately re-trip it.
[metadata] prefer_explicitis on by default: when an album or playlist lists both a clean and an explicit copy of the same track, only the explicit one is downloaded. Costs no extra API calls, only a later start (every track is resolved before any of them download, instead of as each one resolves). Set it tofalsein an existing config to keep both.
- Targets current Python (3.14+) only; every dependency is on a current release with wheels for 3.14. Pillow was capped below 11, which has no 3.14 wheels and failed to build (#1032, #953, #904).
pyproject.tomluses PEP 621 metadata (Poetry 2).appdirsandaiodnsare no longer dependencies.aiodnsforced Windows onto an event loop that cannot run ffmpeg, so conversions failed there (#729).- HTTP(S)_PROXY / ALL_PROXY environment variables are honoured (#961).
Every request goes through one kind of session, so
--no-ssl-verifynow also covers the Qobuz app id lookup (it was ignored there), and last.fm and that lookup time out on a stalled connection like the rest. A certificate error explains the options in every command, not only inurlandfile. - Install from GitHub; the update notice points at this fork instead of telling
you to
pip install streamrip, which would install upstream. - The CLI command is
streamrip, notrip: short names likeripcollide with other tools and local shell aliases/functions too easily.
- Albums download again after Qobuz stopped returning track lists from
album/getandplaylist/get(#1012, PR #1013). - Token login: the prompt asks for a user id and
user_auth_tokenbecause the password login is behind a captcha now (#954, #956, #899, #854), or streamrip opens a browser and takes them from your login there (PR #955). - A stale app id/secret is re-fetched automatically (PR #1031); failed logins close their session (PR #1005); credentials are kept out of logs and errors (PR #1037).
- API errors (e.g. the intermittent Algolia search failure) are reported or
retried instead of
AssertionError(PR #1037). - Free accounts can download albums they purchased (#673, PR #1007).
- Playlists longer than 500 tracks are complete (PR #847).
- The album edition ("Deluxe Edition", "2011 Remaster") is kept in the title,
tags and folder name, so editions no longer merge (#902, #867,
PR #1027);
{version}and{tracktotal}are available infolder_format. - Single tracks of multi-disc albums go into their
Disc Nfolder (PR #1026). - Missing performer/ISRC/genre no longer crash a track (#668).
download_bookletsworks: an album's PDF booklets are saved in its folder. The option was there (and on) but nothing downloaded them since v1.
- Lossless tracks download as FLAC again instead of AAC 320 (#966, #897, #968, PR #1017), and hi-res tracks served as MPEG-DASH are downloaded and remuxed instead of silently downgrading to AAC (#974, PR #998).
- Highest quality first, then one step down at a time. Since late April 2026 no
single Tidal client is served everything (#966): the default one gets FLAC
16/44.1 for every lossless release but never hi-res; the hi-res one gets 24-bit
FLAC (measured up to 192 kHz) for hi-res releases but only AAC 320 for the rest.
streamrip logs in with both (
hires_client, now on by default: a second login, asked for once) and asks the hi-res client first for every track Tidal doesn't mark as lacking a hi-res master (theHIRES_LOSSLESStag), then the default one. A hi-res login from before is moved to its own config fields. It is only used withquality = 3, so--quality 2needs no second login. - New defaults ask for the best each source has: Tidal
quality = 3, Qobuzquality = 4(Deezer was already at FLAC). Existing configs keep their values. - Folder names of hi-res albums show the real format (
[24B-96kHz]) instead of always[16B-44.1kHz]: Tidal's album data only says LOSSLESS, so the format is read from the playback info of one track (one extra request per hi-res album). - Tracks without lyrics are no longer dropped, and lyrics errors never abort a track (#983, #959, #866, PR #1036, PR #1024).
- Share links ending in
/uwork (PR #911). - Expired or revoked logins offer a fresh login instead of a traceback (#896, #906, #901, PR #1029); refreshed tokens are saved.
nullcopyright no longer crashes (PR #979); unknown quality values (HI_RES_LOSSLESS) no longer raiseKeyError.- Search returned nothing when there was exactly one hit, which broke last.fm playlists on Tidal.
- The "requested HI_RES but Tidal only has LOSSLESS" warning no longer appears
for
quality = 3, which means "best available": it is only shown when lossless was requested and a lossy stream came back.hires_clientonly matters together withquality = 3. - API requests retry connection errors, timeouts, 5xx and 429 (up to four
attempts with backoff) instead of dropping the track. A 429 pauses every
request and honours
Retry-After. - Albums with a
nullcopyright no longer fail: the fix for it (upstream PR #979) only reached the code for single tracks. Folder names sayFLAC/AACand44.1kHzlike the other sources, instead ofMP4and44100kHz. - Music videos in albums and playlists are left out instead of each failing as
a track (and being retried by
streamrip repairforever); a playlist with videos no longer loses its last tracks when paging.
- The quality is limited to what the subscription allows instead of failing with
"HiFi is required for quality 2" (#1015);
--quality 3/4no longer raisesIndexError. - Playlists (including private ones) fall back to the gw API when the public API refuses them (#827, PR #973, PR #945).
- New
link.deezer.com/s/...share links (#865, #818, PR #887). - Redirected album ids are followed (#893).
- Delisted tracks are recovered through their fallback track; the retired CDN is no longer used (PR #1028).
- Loved tracks (
/profile/<id>/loved) can be downloaded (PR #1008, PR #987). - Synced lyrics are embedded;
[downloads] lyricsturns lyrics off (PR #984). - All artists are credited (#853, PR #994, PR #907); missing fields no longer crash parsing (PR #944).
- Connection pool warnings are gone (PR #997); album metadata is cached (PR #1000); blocking calls no longer stall other downloads.
- Tracks you uploaded to Deezer yourself (negative ids, no album) are skipped in playlists and loved tracks instead of each failing (PR #832).
- Encrypted tracks are decrypted as they arrive instead of being held in memory whole, and written in large chunks instead of 6 KB at a time; logging in no longer fetches the account's profile a second time.
- The client id is found again (#1038); tracks without an MP3 HLS stream use the progressive stream or are skipped instead of crashing.
- HLS segments are reassembled in their original order regardless of which one finishes downloading first; concurrent, out-of-order completion produced skips and reordered audio (#848, #633).
- Those segments are removed from the temp dir afterwards (also when one fails) instead of piling up, and a failed join shows ffmpeg's error.
- Tracks are tagged disc 1 instead of disc 0.
- Failed downloads are no longer marked as downloaded, resolve failures are
recorded (whatever the error, for single tracks too), and
streamrip repairretries everything in the failed database (PR #1023). A track skipped after a 403 (e.g. a geoblocked Deezer track in an otherwise public playlist) no longer leaves the run in a state where cleanup trips over the file it never wrote (#677, #701). - Up to four attempts with backoff; retries resume the partial file (#951, #1022, PR #1009). Downloads run in a worker thread so they no longer block each other (PR #982).
- A failed conversion keeps the original, says why (ffmpeg's own error), leaves
no temp file behind and still records the download
(#1010); AAC falls back to ffmpeg's own encoder (PR #990); lossy
conversions honour
lossy_bitrate(#823, PR #960); OGG/Opus keep cover art (PR #992); new AIFF target (PR #1006); ffmpeg can no longer break the terminal (PR #996). [metadata] excludeworks (#850) and drops only the tags it names; MP3s are written as ID3v2.3 as intended. M4A files get their composer tag (it was written to the year's atom and lost), and MP3s no longer get the album description as their grouping (TIT1) or an empty lyrics frame.restrict_charactersapplies to every folder streamrip names: a single track saved withadd_singles_to_folderlands in its album's folder instead of one beside it, and playlist folders follow it too.- Long titles no longer fail with "File name too long" (#856, #859,
PR #860); playlist names with
/no longer create folders (PR #1004). - Artwork downloads work with current aiohttp headers (#941).
track_formatgains{album},{albumtitle}and{discnumber}(PR #826).- Error messages include the exception type (#938); Ctrl-C stops cleanly and removes temporary artwork (PR #1025).
- CDNs sending more than 100 headers no longer break downloads (PR #1033).
- Tracks with multiple credited artists (Tidal, Deezer) get a real multi-valued ARTIST tag (separate FLAC/Vorbis fields, a proper multi-value MP4 atom, "/"-joined for ID3v2.3) instead of one "A, B" string that players had to re-split themselves, and often got wrong.
- If a lossless copy of a track is already on disk, a lossy (AAC/MP3) copy
of it is no longer downloaded; if the lossy copy already exists and a
lossless one lands afterwards (also after a lossless conversion), the lossy
one is removed. Avoids ending up with both a FLAC and an AAC copy of the
same track after a quality setting change or a re-run. A file only counts
as a copy when its title and album tags match, not just its name, and ALAC
.m4afiles are recognised as lossless. - New
[metadata] prefer_explicit: when an album or playlist lists both a clean and an explicit copy of the same track, only the explicit one is downloaded. Off by default. With it on, every track is resolved before the first one starts downloading, so downloads start a bit later; the number of API calls stays the same. Also fixed: Qobuz tracks were never marked explicit (parental_warningwas ignored), which this option depends on. - A connection that goes quiet for 30 seconds times out (all sources) instead of holding its album for aiohttp's default of five minutes.
- Artist downloads keep four albums in flight with a sliding window instead of waiting for the slowest album of every batch of four, and one failing album no longer aborts the rest. At most four tracks of an album are resolved at once, so downloads start after the first few tracks instead of after the metadata, lyrics and stream lookups of every track.
- An album whose tracks are all in the database is skipped without fetching a cover or creating a folder, and skipped tracks are reported once per album instead of once per track.
- New
streamrip database clear downloads|failed|allforgets what was downloaded, for when you deleted the files and want them again. - An unknown release date gives the year "Unknown" instead of "Unkn" (in tags and folder names), and no date tag instead of the text "Unknown".
- The repeats filter no longer crashes an artist download on an album whose
title starts with a bracket, e.g. "(What's the Story) Morning Glory?";
[qobuz_filters] non_albumsnow actually skips single-track releases. - Label downloads use the same sliding window as artists, and one failing album no longer aborts the label.
- Playlists resolve through a sliding window too, instead of batches of 20 that
waited for their slowest track. A playlist track that fails is recorded for
streamrip repairlike an album track. For last.fm playlists, one search error no longer aborts the playlist, and falls back to the fallback source. - A playlist downloaded with
set_playlist_to_albumshows up as one album in Navidrome, Plex and the like: its tracks share the album artist "Various Artists" and are marked as a compilation, instead of splitting into one album per artist (PR #738). Withrenumber_playlist_tracks, the disc number and track total follow the playlist too. - One URL that fails to resolve no longer stops the others;
streamrip filekeeps the order of its URLs;search --firstwith no results no longer crashes;database browse failedlines its columns up with the headers.
- Options that never did anything are gone:
[tidal] download_videos,[deezer] use_deezloaderanddeezloader_warnings, the[youtube]section,[cli] text_outputand[soundcloud] quality(SoundCloud has one quality). - Options that did the same thing are merged:
[downloads] concurrency = falseismax_connections = 1;[qobuz_filters]is[artist_filters], since it applies to every source, and itsnon_studio_albums(extras plus various-artists compilations) is part ofextras. - The config version is 2.3.3: an existing config is updated automatically on the next run, keeping your settings, including those of merged options.
[cli] max_search_resultsis used (it was ignored): it's the default forsearch --num-results, in the interactive menu too.
- CI on Python 3.14, install smoke test on Linux, Windows and macOS, CodeQL on the right branch; all actions pinned to SHAs.
- Renovate keeps dependencies current; Dependabot handles security updates.