Describe the bug
Tidal now serves HI_RES_LOSSLESS (and in some regions/albums also LOSSLESS) as manifestMimeType: application/dash+xml — a multi-segment DASH-MPD manifest pointing at fragmented MP4 (fMP4) segments containing the FLAC stream. Streamrip 2.2.0 and the current dev branch only parse the older application/vnd.tidal.bts (single-URL BTS-JSON) format. The XML manifest raises JSONDecodeError inside get_downloadable, the except block calls itself recursively at quality n-1, and Tidal's API then server-side downgrades LOSSLESS → HIGH (AAC 320 BTS-JSON), which parses successfully — so the user gets a silent fallback to a .m4a file with no error.
This is the underlying cause of #970, #966, #892, and the long thread in #801. None of those reports identified the DASH transition; they all attribute it to client_id/client_secret rotation. The credentials are not the problem — the manifest format is.
Root cause
streamrip/client/tidal.py (line ~165 on dev and 2.2.0):
async def get_downloadable(self, track_id: str, quality: int):
...
resp = await self._api_request(
f"tracks/{track_id}/playbackinfopostpaywall", params
)
try:
manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
except KeyError:
raise Exception(resp["userMessage"])
except JSONDecodeError:
logger.warning(
f"Failed to get manifest for {track_id}. Retrying with lower quality."
)
return await self.get_downloadable(track_id, quality - 1)
...
return TidalDownloadable(
...
url=manifest["urls"][0],
codec=manifest["codecs"],
...
)
The code assumes resp["manifest"] base64-decodes to JSON. For Hi-Res, it now decodes to:
<?xml version='1.0' encoding='UTF-8'?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" ...>
<Period id="0">
<AdaptationSet id="0" contentType="audio" mimeType="audio/mp4" ...>
<Representation id="FLAC_HIRES,96000,24" codecs="flac" bandwidth="2773973" audioSamplingRate="96000">
<SegmentTemplate timescale="96000"
initialization="https://sp-ad-fa.audio.tidal.com/mediatracks/.../0.mp4?token=..."
media="https://sp-ad-fa.audio.tidal.com/mediatracks/.../$Number$.mp4?token=..."
startNumber="1">
<SegmentTimeline>
<S d="380928" r="69"/>
<S d="128210"/>
</SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>
The response also carries manifestMimeType: application/dash+xml which the current code never inspects.
Reproduce
# Use an album you know is Hi-Res in the Tidal app:
rip --no-db --quality 3 url "https://tidal.com/album/505811027"
Result: 10 .m4a files at AAC 320, no warning about quality loss beyond the (silent-on-the-surface) "Failed to get manifest. Retrying with lower quality." log line.
Verify with curl that Tidal does have FLAC for the same tracks:
TOKEN=$(grep '^access_token' "$HOME/Library/Application Support/streamrip/config.toml" | sed 's/access_token = "//;s/"$//')
curl -sS "https://api.tidal.com/v1/tracks/505811029/playbackinfopostpaywall?audioquality=HI_RES_LOSSLESS&playbackmode=STREAM&assetpresentation=FULL&countryCode=US" \
-H "Authorization: Bearer $TOKEN" | python3 -c "
import sys, json, base64
d = json.load(sys.stdin)
print('audioQuality:', d['audioQuality'])
print('manifestMimeType:', d['manifestMimeType'])
print('bitDepth:', d.get('bitDepth'), 'sampleRate:', d.get('sampleRate'))
print(base64.b64decode(d['manifest']).decode()[:300])
"
Expected output: audioQuality: HI_RES_LOSSLESS, manifestMimeType: application/dash+xml, bitDepth: 24, sampleRate: 96000, followed by the MPD XML.
Proposed fix
get_downloadable should dispatch on resp.get("manifestMimeType"):
application/vnd.tidal.bts → existing JSON path (unchanged)
application/dash+xml → parse MPD, build a downloader that:
- Fetches
SegmentTemplate/@initialization (the init segment)
- Fetches each numbered media segment (
media URL with $Number$ substituted), startNumber through startNumber + sum((S.r or 0) + 1)
- Concatenates init + segments into a single fMP4 stream
- Optionally remuxes to native FLAC via
ffmpeg -i in.mp4 -map 0:a -c:a copy out.flac (lossless, no re-encode)
No DRM is in play for the FLAC AdaptationSet (no <ContentProtection> element), so this is a plain HTTP segmented download. fMP4-FLAC is a standard container; ffmpeg copies the FLAC frames out without modification.
A working reference implementation (~250 LOC of stdlib + ffmpeg + mutagen) — happy to attach inline or open a PR. The MPD parser is just xml.etree.ElementTree against the namespace urn:mpeg:dash:schema:mpd:2011, and segment downloads are plain urllib.request (no special headers needed beyond the signed URL).
Workaround
Until this lands, use the standalone script above or any DASH-aware downloader (e.g. yt-dlp with --allow-unplayable-formats, though that re-muxes differently).
Environment
- streamrip 2.2.0 (from
nathom/streamrip@v2.2.0 via pipx)
dev branch HEAD as of 2026-05-20: same broken code path
- Tested on macOS (Apple Silicon), but the bug is in protocol handling — OS-independent
- Tidal HiFi Plus account, US region, account confirmed to deliver FLAC to the Tidal app for the same albums
Describe the bug
Tidal now serves
HI_RES_LOSSLESS(and in some regions/albums alsoLOSSLESS) asmanifestMimeType: application/dash+xml— a multi-segment DASH-MPD manifest pointing at fragmented MP4 (fMP4) segments containing the FLAC stream. Streamrip 2.2.0 and the currentdevbranch only parse the olderapplication/vnd.tidal.bts(single-URL BTS-JSON) format. The XML manifest raisesJSONDecodeErrorinsideget_downloadable, theexceptblock calls itself recursively at qualityn-1, and Tidal's API then server-side downgradesLOSSLESS→HIGH(AAC 320 BTS-JSON), which parses successfully — so the user gets a silent fallback to a.m4afile with no error.This is the underlying cause of #970, #966, #892, and the long thread in #801. None of those reports identified the DASH transition; they all attribute it to
client_id/client_secretrotation. The credentials are not the problem — the manifest format is.Root cause
streamrip/client/tidal.py(line ~165 ondevand 2.2.0):The code assumes
resp["manifest"]base64-decodes to JSON. For Hi-Res, it now decodes to:The response also carries
manifestMimeType: application/dash+xmlwhich the current code never inspects.Reproduce
Result: 10
.m4afiles at AAC 320, no warning about quality loss beyond the (silent-on-the-surface) "Failed to get manifest. Retrying with lower quality." log line.Verify with
curlthat Tidal does have FLAC for the same tracks:Expected output:
audioQuality: HI_RES_LOSSLESS,manifestMimeType: application/dash+xml,bitDepth: 24,sampleRate: 96000, followed by the MPD XML.Proposed fix
get_downloadableshould dispatch onresp.get("manifestMimeType"):application/vnd.tidal.bts→ existing JSON path (unchanged)application/dash+xml→ parse MPD, build a downloader that:SegmentTemplate/@initialization(the init segment)mediaURL with$Number$substituted),startNumberthroughstartNumber + sum((S.r or 0) + 1)ffmpeg -i in.mp4 -map 0:a -c:a copy out.flac(lossless, no re-encode)No DRM is in play for the FLAC AdaptationSet (no
<ContentProtection>element), so this is a plain HTTP segmented download. fMP4-FLAC is a standard container; ffmpeg copies the FLAC frames out without modification.A working reference implementation (~250 LOC of stdlib + ffmpeg + mutagen) — happy to attach inline or open a PR. The MPD parser is just
xml.etree.ElementTreeagainst the namespaceurn:mpeg:dash:schema:mpd:2011, and segment downloads are plainurllib.request(no special headers needed beyond the signed URL).Workaround
Until this lands, use the standalone script above or any DASH-aware downloader (e.g.
yt-dlpwith--allow-unplayable-formats, though that re-muxes differently).Environment
nathom/streamrip@v2.2.0via pipx)devbranch HEAD as of 2026-05-20: same broken code path