Skip to content

TIDAL full tracks lossless: Sign in with TIDAL (browser, built-in client); Sound quality dialog removed - #255

Open
gillohner wants to merge 7 commits into
mainfrom
sources/tidal-lossless
Open

gillohner wants to merge 7 commits into
mainfrom
sources/tidal-lossless

Conversation

@gillohner

Copy link
Copy Markdown
Owner

TIDAL full tracks arrived as AAC (AAC → 24/44.1 lossy) with the device-code sign-in. This adds TIDAL's browser sign-in (PKCE, python-tidal's login_pkce) as the Recommended way, with a built-in client, and removes the Sound quality dialog.

What changes

  • Built-in clients: PM_TIDAL_PKCE_CLIENT_* (new) and PM_TIDAL_DEVICE_CLIENT_* (restored after No built-in TIDAL device client: full tracks only with one the owner enters #216: REVISION 3 makes the device-code sign-in with the built-in client the fallback, and existing device sessions need it to renew). release.yml passes the secrets only to the build steps (desktop npm ci split out so install scripts never see them) and to the image as BuildKit secrets (no-cache-filters: build); the Dockerfile reads them in the build stage's one RUN, nothing in any layer or build arg. build.rs writes them masked into $OUT_DIR (src/client_mask.rs); no values in the repo. Dev builds without them: the browser way is unavailable, the code sign-in remains.
  • Sign in with TIDAL: POST /v1/sources/tidal/unofficial/browser starts a per-person 10-minute attempt and returns the login.tidal.com authorize URL (S256, redirect https://tidal.com/android/login/auth); .../browser/finish takes the landing address or the bare code, checks https + tidal.com host first, exchanges the code, stores the session marked client: pkce. Refresh uses the PKCE client. Stored sessions without the marker are device sessions and keep working.
  • Playback: one log line per opened track with TIDAL's answer (audioQuality, manifestMimeType, codecs, bit depth, sample rate, sign-in kind; no URLs or tokens). AAC for HI_RES_LOSSLESS is asked again as LOSSLESS. A sign-in that still gets AAC says so on its health line with what to do. Stream/segment errors no longer carry URLs.
  • Chooser: Sign in with TIDAL (Recommended), then Sign in with a code (fallback, may be AAC only); Advanced disclosure without badge: official app (previews), own device client, Paste a token; "A sign-in from another app on this machine" only when found. Device-code users see "Sign in with TIDAL again for lossless" with the button.
  • UI removal: Sound quality dialog and its helpers/CSS; the signal-path panel's "Quality choices…" / "Now playing" buttons and their footer. "All DSP settings" stays.
  • Docs: TIDAL guide, STREAMING-PROVIDERS, deploy.md, api.md; CHANGELOG under Unreleased.

Reference read: tamland/python-tidal cce10953477362c4383731ad6a8f4ef6296a6168 (tidalapi/session.py, media.py, request.py).

Checks

  • fmt; clippy -D warnings pm-api; rustdoc -D warnings; cargo test -p pm-api 426 passed / 0 failed / 4 ignored (new: authorize URL + challenge, code exchange, PKCE refresh, paste parsing, order/Recommended, per-person isolation, BTS FLAC / DASH FLAC / DASH AAC fixtures + LOSSLESS retry + health text, old session loads, masking with fake inputs, no secret in logs).
  • UI on a scratch node: ui-stream-setup 140, ui-signin 12, ui-sweep-followups 85, ui-tidal-login pass. Workflow YAML parses; secret scan passes with fake built-in values.

Not yet measured

Whether the browser session really gets FLAC needs one real sign-in by the owner on the edge image; the result goes here after the NUC check.

gillohner and others added 7 commits November 4, 2026 14:06
Owner, 2026-11-04: full tracks get two built-in clients again, never in the
repository. build.rs reads PM_TIDAL_PKCE_CLIENT_ID / _SECRET (the browser
sign-in's) and PM_TIDAL_DEVICE_CLIENT_ID / _SECRET (the code sign-in's) and
writes each pair, only when both values are set, as masked bytes into
$OUT_DIR/tidal_clients.rs; src/client_mask.rs is the one helper build.rs and
project_apps share. Masking keeps the values out of the binary's plain strings;
it is not a security boundary.

- project_apps: tidal_pkce_client() and tidal_device_client().
- tidal_unofficial::client_of: the owner's client (Settings, then [tidal]
  config, then PM_TIDAL_UNOFFICIAL_CLIENT_*), else the build's ("builtin"),
  so sessions made with the old built-in device client renew again.
- release.yml: the four secrets only on the three steps that build (desktop,
  Android, binaries) and on the image build as BuildKit secrets with
  no-cache-filters: build; the desktop's npm ci moves to its own step so install
  scripts never see them. Nothing at workflow level; action pins unchanged.
- Dockerfile: secret mounts read in the build stage's one RUN; no ARG or ENV,
  nothing in the final image. build-armv7.sh forwards them by name.

Tests: masking with fake inputs present and absent (no plain value in the
generated file, read back exactly, blank pairs give none); the build inputs come
only from the build environment, scoped as above, and no built-in value is
plain in the source (replaces the test that no device client is fed in).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n, the answer logged

On the owner's node TIDAL sent the device-code session AAC for HI_RES_LOSSLESS.
python-tidal (tamland/python-tidal at cce10953) says only its PKCE client gets
hi-res; this matches its login_pkce, pkce_get_auth_token and token_refresh.

Sign in with TIDAL (browser sign-in), per person:
- POST /v1/sources/tidal/unofficial/browser keeps a short-lived attempt (code
  verifier: 32 random bytes base64url; client_unique_key: 64 random bits hex; ten
  minutes) in the person's own state and answers the login.tidal.com authorize
  address: response_type, redirect_uri https://tidal.com/android/login/auth,
  client_id, lang, appMode=android, client_unique_key, S256 challenge,
  restrict_signup, in python-tidal's order.
- POST .../browser/finish {text} takes the pasted landing address or the bare
  code. The host is checked first (https, tidal.com or a subdomain); a wrong
  host, http, no code, TIDAL's error, an unknown (409) or expired (410) attempt
  and a code TIDAL refuses each say so in plain words. The exchange sends code,
  client_id, redirect_uri, scope, code_verifier and client_unique_key (no secret,
  as python-tidal); the session is kept marked client = pkce, the way remembered.
- Sessions say which client renews them (ClientKind, serde default: a session
  stored before is a device session, so nobody is signed out). A browser session
  renews with the PKCE id and secret, no scope; a device one as before. A
  python-tidal / tidal-dl-ng file with is_pkce imports as a browser session.

Playback (open_blocking): one info line per opened track with what TIDAL
answered (audioQuality, manifestMimeType, BTS / DASH codecs, bitDepth,
sampleRate, the sign-in kind; never a URL or token), python-tidal's request
headers, and AAC for HI_RES_LOSSLESS asked again as LOSSLESS before it plays.
The health line of a sign-in that still gets AAC says so with what to do
("... Sign in with TIDAL again for lossless"; lossy: true) instead of "full
tracks are playing". Hi-res DASH (FLAC in fragmented MP4) goes through the DASH
reader into the decoder, which names the codec and depth from the stream (the
signal path's source format). Stream and segment errors no longer carry URLs.

The chooser (tidal::sign_in): Sign in with TIDAL first and Recommended (when the
build carries its client), Sign in with a code second (a fallback TIDAL may
send AAC only), a sign-in from another app only when one is found, then under
Advanced (Way::advanced) the official app, Your own device client and Paste a
token. A build without its clients lists both first ways as unavailable,
saying why.

Tests against mock TIDAL servers: authorize address and challenge, the
exchange's parameters, renewal with the PKCE client, paste parsing (address,
bare code, other host, look-alike hosts, http, no code, TIDAL's error), no and
expired attempts, Recommended flag and order, per-person isolation, answer
fixtures BTS FLAC / DASH FLAC hi-res / DASH AAC, the LOSSLESS retry, the
health texts, an old stored device session that loads and renews with the
device client, and no secret, token, verifier or CDN address in any log line
(captured at trace level).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner's ask (2026-11-04). Gone from the web UI: the Sound quality dialog
(qualitySheet) and what only it used (focusQualityControl, conversionWhy, the
.qsheet and .flash styles); in the signal-path panel the footer with "Quality
choices…" (#qpquality) and "Now playing" (#qpnow), their handlers and the
footer's style. Nothing else opened the dialog: Now playing's quality label
(#npquality) now always opens Details (the signal path), as it did without a
format. The panel keeps "All DSP settings"; each source's quality setting stays
on its Sources page (GET/PUT /v1/sources/{source}/quality is unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e code sign-in second

The web UI for the new chooser (owner, 2026-11-04), order and badge from the
backend:
- Sign in with TIDAL: one button opens TIDAL's sign-in in a new tab (opened at
  the click so no popup blocker stops it), then step 2 asks for the address of
  the tidal.com page you land on, with the problem in place when it isn't one;
  then done. Step cards as elsewhere. No client id or secret to enter.
- The signin chooser (all sources) shows ways marked advanced under an
  "Advanced" disclosure, open when one of them is picked; TIDAL's official app,
  Your own device client (the client step card, Check, paste splitting) and
  Paste a token (which takes the file too) are there, without a badge.
- A code sign-in sees one line, "Sign in with TIDAL again for lossless", with
  the button. A sign-in that gets AAC says so on its Health line (pill AAC,
  status "AAC only") instead of "Working: full tracks are playing".
- tools/ui-stream-setup.js: the chooser's order, the badge, the closed
  Advanced, the new tab and the paste step (a wrong address, then signed in),
  the one "again" line, the AAC health line, then a build without its clients
  (both first ways unavailable, saying why) and your own device client through
  to the code; desktop and phone width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…clients

- guides/tidal-full-tracks.md: the ways in their new order, Sign in with TIDAL
  step by step (what the landing page looks like, what is read from the
  address, the errors), the code sign-in and when TIDAL sends it AAC, your own
  device client under Advanced and the client order, what each track logs, and
  that old sessions keep working.
- STREAMING-PROVIDERS.md: the full-tracks row (both built-in clients from
  release secrets, the browser sign-in as python-tidal's login_pkce, the
  session's client, the LOSSLESS retry, the log line), the Advanced note.
- deploy.md: what a release build carries and how a local image build passes
  the clients (BuildKit secrets, --no-cache-filter build).
- api.md: the two browser sign-in endpoints and the status fields.
- CHANGELOG under Unreleased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to a client

Review fixes for #255 (round 1):
- The vault carries a full-tracks session under the app of the client that
  made it: a code sign-in under the device client's id, as before (device
  sessions synced already still match), a Sign in with TIDAL session under
  `pkce:` and the browser client's id. Bridges get an optional per-session
  app (`app_of_session`); the sync compares the vault copy against the
  node's app for that kind, records the app of the copy it synced, and
  checks "lost to a change of app" against the vault copy's kind. A node
  takes only a session it can renew, and a code sign-in made on a node
  without the browser client doesn't replace a lossless session elsewhere.
  Test with both kinds on three nodes (both clients, no browser client,
  another device client), and the sync decisions that follow.
- Imports say they renew only when the node has the client of their kind:
  a device client for device sessions, this build's browser client for
  python-tidal's browser (PKCE) ones; an expired one without it is refused
  in plain words. Test.
- The AAC health line suggests "Sign in with TIDAL again" only for a code
  sign-in on a build with the browser client; an imported or pasted one
  gets "Sign in with TIDAL for lossless", a build without the client says
  what lossless needs. Tests.
- Stale wording: docs/api.md (quality, the ways that switch full tracks
  on), STREAMING-PROVIDERS.md (the ways), the import test's message; the
  CHANGELOG entry says the above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ess field

Review fixes for #255 (round 1):
- The full-tracks status says "AAC only" only for a device session; a
  browser session whose last track came as AAC stays Working, and its
  Health line says that track may have no lossless version.
- After Sign in with TIDAL is pressed and the chooser is drawn again, the
  address field has the focus, so whoever comes back from TIDAL's tab by
  keyboard or screen reader is on it.
- tools/ui-stream-setup.js checks both, desktop and phone width.

Co-Authored-By: Claude Opus 5.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.

1 participant