Skip to content

Latest commit

 

History

History
949 lines (807 loc) · 54.6 KB

CHANGELOG.md

File metadata and controls

949 lines (807 loc) · 54.6 KB

Changelog

All notable changes in this fork of nathom/streamrip. Numbers can refer to upstream issues and pull requests.

2.7.1

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_CONFIG secret

2.7.0

  • 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

2.6.9

  • 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

2.6.8

  • docs: medium is 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 as slow's ~80, only without slow's own 18-hour clean run
  • docs: --pace --help is far shorter, pointing at [downloads] pacing in 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 install on the released wheel instead of a git source install where that is the install in use

2.6.7

  • feat: [downloads] create_album_folder = false puts tracks directly in the downloads folder instead of a per-album subfolder (based on #90 by @Xenxia)
  • feat: [downloads] save_lrc also writes lyrics to an .lrc file
  • 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)

2.6.6

  • 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.
  • --pace gains medium: a faster, unproven ~150 tracks an hour for a session, between fast (no waiting, not for hours on end) and the proven slow (~80 an hour). auto still only ever resolves to fast or slow.

2.6.5

  • 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 fast and --pace slow choose one. pacing in [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 --pace and --quality also 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 path prints only the path.
  • checks/run.py runs 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.

2.6.2

  • 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].

2.6.1

  • The browser's messages ("Opening in existing browser session.", zygote errors) are no longer shown in the terminal when a login page opens. (#81)

2.6.0

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.

Upgrading

  • 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 reset writes 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 123 instead of tidal_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 the browser extra now: pip install ".[browser]". The installers include it. The ssl extra is gone, as certifi is always installed.

Container image

  • A container image for Podman and Docker, built from a Containerfile and 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_CHECK environment variable, which turns the check off anywhere (any value but empty, 0, false or no), next to no_update_check in 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.

Stopping

  • SIGTERM, what podman stop, docker stop, systemd and kill send, 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, so podman stop waited 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.

Logging in

  • 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.

Speed

  • streamrip --version starts 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 --help and --version no 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 Done instead of Overall, 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.

Python

  • Native Python 3.15 throughout: paths are pathlib.Path instead of os.path strings, generics use PEP 695, asyncio.timeout and create_task replace wait_for and ensure_future, constants that are only read are frozendict, and the from __future__ imports and quoted annotations are gone. Imports that only annotations need are lazy (PEP 810) instead of behind TYPE_CHECKING, and comprehensions unpack (PEP 798). Ruff now enforces the modern forms (UP, PTH, B, C4, FURB, PERF, PIE, RET, SIM, TC).

uv

  • The project is managed with uv instead of Poetry: uv.lock replaces poetry.lock, the dev tools are a [dependency-groups] group, and uv_build builds the package. CI, releases, Renovate and Dependabot use it too.
  • The installers run uv sync, so they install the versions of uv.lock and remake a .venv that is not on Python 3.15 by themselves. They include ffmpeg now (the ffmpeg extra), so it no longer has to be installed separately. The container image is built from uv.lock as 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.

Dependencies

  • click-help-colors is gone: it needs click.MultiCommand, which Click 9 removes. --help looks the same, with the same colors and NO_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).

Documentation and tests

  • 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, parametrize and tmp_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.yml and tests/fixtures/.

2.5.1

SoundCloud

  • 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)

Dependencies

  • 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.toml allows 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-action 31.11.2 and CodeQL 4.39.0.

2.5.0

Thanks to @berettavexee for the unified progress bar below, and to @Multipixelone for the Nix flake below.

Progress

  • 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)

Nix

  • A Nix flake (nix build, nix run, nix develop, nix flake check) for x86_64-linux, with nixpkgs pinned in flake.lock, on Python 3.14: the package with an explicit dependency list kept in sync with pyproject.toml, a development shell with the same dependencies plus Poetry and Ruff, and FFmpeg, Deno and Chromium on PATH. 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 runs nix flake check on every change. (#46)

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, 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 (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 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.

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.

2.4.10

Security

  • 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.

Downloading

  • SoundCloud tracks are skipped when already downloaded. They were downloaded again on every run.
  • The downloads database keeps ids per source (tidal_2430924980, not 2430924980). 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 downloads forgets them. streamrip database browse downloads shows 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 repair removed both. An existing database is converted on first use.

Configuration

  • 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 = true under [cli] turns the update check off. It is commented out in the config by default.

Command line

  • A config that does not load ends the command with exit code 1, not 0.
  • The update notice says its command needs git.

Tests

  • 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.

2.4.9

Thanks to @berettavexee for the resume check, the SoundCloud fallback, the Deezer playlist fix and the traceback fix below.

Search

  • 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 with SPACE; ENTER downloads 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.

Downloading

  • 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 with IncompleteDownloadError, so the next attempt starts from scratch instead of resuming onto the same mismatch (PR #28, by @berettavexee).

Deezer

  • 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).

SoundCloud

  • 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).

Command line

  • -v tracebacks no longer print local variables. They included the Deezer arl after 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).

Python

  • streamrip supports Python 3.14 and 3.15, not newer: requires-python is now >=3.14,<3.16. CI runs the tests and the install check on both.

2.4.8

Thanks to @berettavexee for the Deezer quality fix and the FLAC cover fix below.

Deezer

  • Logging in no longer means digging the arl cookie out of DevTools. streamrip offers a choice: an isolated browser window where you log in yourself and streamrip keeps only the arl cookie 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 1 gave 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.

Tagging

  • Qobuz writes several track artists as separate ARTIST values, 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 ALBUMARTIST values for every source, instead of one A, B, C string. 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).

Tidal

  • 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.

SoundCloud

  • Small playlists, which come with every track's metadata up front, no longer fail every track with "not enough values to unpack".

Search

  • The preview in streamrip search shows 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.

Logging in and progress

  • 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.

Config

  • [misc] check_for_updates is 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.

Internals

  • Cleanups: Downloadable is 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 (set QOBUZ_USER_ID and QOBUZ_AUTH_TOKEN).
  • streamrip.metadata no longer exports AlbumSummary, ArtistSummary, TrackSummary, PlaylistSummary and LabelSummary; one Summary replaces them.

2.4.7

Downloading several artists

  • 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 repair when something failed. The empty "Downloading" line no longer stays behind after the run.

Qobuz

  • 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_userid and password_or_token are replaced by user_id and auth_token. Existing users should delete their config file.

Network

  • requests_per_minute spaces 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_minute at 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.

2.4.6

  • 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.

2.4.5

  • 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's encoder stamp, ...) 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 only url. The suggested upgrade command pins the detected release instead of installing whatever dev currently has.

2.4.4

  • 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.

2.4.3

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.

2.4.2

  • 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.

2.4.1

  • [metadata] prefer_explicit is 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 to false in an existing config to keep both.

2.4.0

Installation and dependencies

  • 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.toml uses PEP 621 metadata (Poetry 2).
  • appdirs and aiodns are no longer dependencies. aiodns forced 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-verify now 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 in url and file.
  • 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, not rip: short names like rip collide with other tools and local shell aliases/functions too easily.

Qobuz

  • Albums download again after Qobuz stopped returning track lists from album/get and playlist/get (#1012, PR #1013).
  • Token login: the prompt asks for a user id and user_auth_token because 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 in folder_format.
  • Single tracks of multi-disc albums go into their Disc N folder (PR #1026).
  • Missing performer/ISRC/genre no longer crash a track (#668).
  • download_booklets works: an album's PDF booklets are saved in its folder. The option was there (and on) but nothing downloaded them since v1.

Tidal

  • 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 (the HIRES_LOSSLESS tag), then the default one. A hi-res login from before is moved to its own config fields. It is only used with quality = 3, so --quality 2 needs no second login.
  • New defaults ask for the best each source has: Tidal quality = 3, Qobuz quality = 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 /u work (PR #911).
  • Expired or revoked logins offer a fresh login instead of a traceback (#896, #906, #901, PR #1029); refreshed tokens are saved.
  • null copyright no longer crashes (PR #979); unknown quality values (HI_RES_LOSSLESS) no longer raise KeyError.
  • 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_client only matters together with quality = 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 null copyright no longer fail: the fix for it (upstream PR #979) only reached the code for single tracks. Folder names say FLAC/AAC and 44.1kHz like the other sources, instead of MP4 and 44100kHz.
  • Music videos in albums and playlists are left out instead of each failing as a track (and being retried by streamrip repair forever); a playlist with videos no longer loses its last tracks when paging.

Deezer

  • The quality is limited to what the subscription allows instead of failing with "HiFi is required for quality 2" (#1015); --quality 3/4 no longer raises IndexError.
  • 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] lyrics turns 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.

SoundCloud

  • 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.

Downloads, conversion and tagging

  • Failed downloads are no longer marked as downloaded, resolve failures are recorded (whatever the error, for single tracks too), and streamrip repair retries 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] exclude works (#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_characters applies to every folder streamrip names: a single track saved with add_singles_to_folder lands 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_format gains {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 .m4a files 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_warning was 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|all forgets 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_albums now 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 repair like 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_album shows 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). With renumber_playlist_tracks, the disc number and track total follow the playlist too.
  • One URL that fails to resolve no longer stops the others; streamrip file keeps the order of its URLs; search --first with no results no longer crashes; database browse failed lines its columns up with the headers.

Configuration

  • Options that never did anything are gone: [tidal] download_videos, [deezer] use_deezloader and deezloader_warnings, the [youtube] section, [cli] text_output and [soundcloud] quality (SoundCloud has one quality).
  • Options that did the same thing are merged: [downloads] concurrency = false is max_connections = 1; [qobuz_filters] is [artist_filters], since it applies to every source, and its non_studio_albums (extras plus various-artists compilations) is part of extras.
  • 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_results is used (it was ignored): it's the default for search --num-results, in the interactive menu too.

Development

  • 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.