Skip to content

Fix Tidal 404 handling: share links, missing lyrics, and the crashing handler - #1024

Open
sjbrownrigg wants to merge 4 commits into
nathom:devfrom
sjbrownrigg:fix/tidal-404-logging
Open

Fix Tidal 404 handling: share links, missing lyrics, and the crashing handler#1024
sjbrownrigg wants to merge 4 commits into
nathom:devfrom
sjbrownrigg:fix/tidal-404-logging

Conversation

@sjbrownrigg

@sjbrownrigg sjbrownrigg commented Aug 13, 2026

Copy link
Copy Markdown

The problem

Pasting a link straight from Tidal's share sheet crashes:

$ rip url https://tidal.com/album/152697662/u
...
  File "/usr/lib/python3.12/logging/__init__.py", line 392, in getMessage
    msg = msg % self.args
TypeError: not all arguments converted during string formatting

Two separate bugs stack up here, and together they make the failure very hard to diagnose — the traceback points at logging, and says nothing about the URL.

1. The /u suffix is parsed as the item id

Tidal's share sheet appends /u. URL_REGEX takes the last path segment as the item id, which it has to do because Qobuz album urls look like /<locale>/album/<slug>/<id>. So the suffix wins:

>>> URL_REGEX.match("https://tidal.com/album/152697662/u").groups()
('tidal', 'album', 'u')
>>> URL_REGEX.match("https://tidal.com/album/152697662").groups()
('tidal', 'album', '152697662')

streamrip then requests album u, which 404s.

2. The 404 handler crashes instead of reporting

if resp.status == 404:
    logger.warning("TIDAL: track not found", resp)
    raise NonStreamableError("TIDAL: Track not found")

The message has no format placeholder but an argument is passed, so msg % args raises TypeError as soon as the record is formatted — before the NonStreamableError on the next line is ever reached. Any 404 from the Tidal API surfaces as a logging traceback rather than the intended error. This has been present since b01382f (#623).

3. A track with no lyrics is dropped entirely

get_metadata() fetches lyrics for tracks. The lyrics endpoint 404s for any track nobody has written lyrics for, and _api_request turns that into NonStreamableError — but the lyrics fetch only caught TypeError, so the error escaped get_metadata().

PendingTrack.resolve() reasonably treats a NonStreamableError from get_metadata() as "this track cannot be streamed", so the track was skipped. A track was dropped from an album download purely because it had no lyrics:

WARNING  TIDAL: item not found (404): .../tracks/152697671/lyrics
ERROR    Track 152697671 not available for stream on tidal

On a 12-track album this silently cost 4 tracks.

Note on prior art: #911 by @scratchmex reported and fixed the /u issue first, back in November 2025, and was confirmed by @CharliePalm. Credit for spotting it goes there. I've implemented it differently only because url.rstrip("/u") strips characters, not the suffix — https://tidal.com/album/1u/u becomes .../album/1, and .../track/uuu/u becomes .../track. Tidal ids are numeric in practice so it rarely bites, but the anchored regex avoids it. Happy to close this in favour of #911 with that one line adjusted, if the maintainers prefer — the other two fixes here are independent and can be split out.

What this changes

  • Fix crash when Tidal returns 404 — add the missing placeholder and log resp.url, which is the useful part. A 404 now reports NonStreamableError as intended.
  • Fix Tidal share links ending in /u — strip the suffix from tidal.com urls before matching. Scoped to tidal.com, so Qobuz's /<locale>/album/<slug>/<id> form and Deezer urls are untouched.
  • Don't drop Tidal tracks that have no lyrics — catch NonStreamableError alongside TypeError around the lyrics fetch. Lyrics are optional metadata and must not decide whether audio gets downloaded. Verified against the four tracks above: all four now resolve.

Testing

Added test_tidal_share_url_with_u_suffix, covering /u, /u/ and /browse/.../u. It fails on dev (3 subtest failures) and passes with the fix.

Verified the existing forms still parse to the same ids: tidal.com/browse/track/..., listen.tidal.com/track/..., qobuz.com/fr-fr/album/<slug>/<id>, qobuz.com/us-en/album/name/id123456, deezer.com/en/track/....

pytest tests/ gives 60 passed, 7 skipped, and 1 failure in test_meta.py::test_album_metadata_qobuz (a genre assertion) which also fails unmodified on dev, so it is unrelated to this change.

sjbrownrigg and others added 3 commits August 13, 2026 12:18
logger.warning("TIDAL: track not found", resp) passes an argument to a
message with no format placeholder, so logging raises

    TypeError: not all arguments converted during string formatting

as soon as the record is actually formatted. A 404 from the API -- an
unavailable or region-restricted item, or a bad id -- therefore crashed
with a traceback from inside logging instead of reporting the
NonStreamableError on the next line.

Add the placeholder and log the URL, which is the useful part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tidal's share sheet produces urls like

    https://tidal.com/album/152697662/u

URL_REGEX takes the last path segment as the item id, which it has to do
because Qobuz album urls look like /<locale>/album/<slug>/<id>. So the
trailing "/u" was parsed as the id, streamrip requested album "u", and
the API returned 404.

Strip that suffix before matching. It is specific to tidal.com urls, so
Qobuz and Deezer paths are untouched.

Combined with the 404 handler in the previous commit, the failure was
particularly hard to diagnose: the id was silently wrong, and the 404 it
caused crashed inside logging rather than reporting itself.

Adds a regression test covering the /u, /u/ and /browse/.../u forms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_metadata() fetches lyrics for tracks, and the lyrics endpoint 404s
for any track nobody has written lyrics for -- which is a lot of them.
_api_request turns a 404 into NonStreamableError, but the lyrics fetch
only caught TypeError, so that error escaped get_metadata().

PendingTrack.resolve() understandably treats a NonStreamableError from
get_metadata() as "this track cannot be streamed" and skips it. The
result was that a track was dropped from an album download for the sole
reason that it had no lyrics:

    WARNING  TIDAL: item not found (404): .../tracks/152697671/lyrics
    ERROR    Track 152697671 not available for stream on tidal

Catch NonStreamableError alongside TypeError. Lyrics are optional
metadata and must not decide whether audio gets downloaded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sjbrownrigg sjbrownrigg changed the title Fix Tidal share links ending in /u, and the 404 handler that hid the error Fix Tidal 404 handling: share links, missing lyrics, and the crashing handler Aug 13, 2026
Most tracks have no lyrics, so the lyrics endpoint 404s constantly. That
produced two warnings per track -- one from the 404 handler and one from
the lyrics handler -- for something entirely normal:

    WARNING  TIDAL: item not found (404): .../tracks/152697671/lyrics
    WARNING  Failed to get lyrics for 152697671: TIDAL: Track not found

"Not found" is the expected answer here, not a problem. Worse, the noise
makes a real failure indistinguishable from the routine case.

Add ItemNotFoundError, a subclass of NonStreamableError raised for 404 so
existing handlers are unaffected, and let callers tell "does not exist"
from "failed to fetch":

  - The 404 in _api_request now logs at debug. Callers that care report
    it themselves at the right level, so it was duplicated anyway.
  - A 404 fetching lyrics logs at debug: nothing was expected.
  - Any other lyrics failure still warns, because lyrics that should have
    been there and could not be fetched is worth knowing about.

Verified against three tracks known to have no lyrics: no warning-level
output at all, and a simulated non-404 lyrics failure still warns while
leaving the track downloadable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants