Skip to content

feat: tidal joins the provider selector, with its protected streams refused rather than worked around - #11

Merged
neur0map merged 8 commits into
Ryoku-dev:mainfrom
eximora-private:feat/tidal-provider
Oct 17, 2026
Merged

neur0map merged 8 commits into
Ryoku-dev:mainfrom
eximora-private:feat/tidal-provider

Conversation

@eximora-private

@eximora-private eximora-private commented Oct 14, 2026 •

Copy link
Copy Markdown

What changed

TIDAL is a fourth provider, reachable from the same selector as YouTube Music, Spotify and SoundCloud.

  • crates/tidal (new): OAuth device and PKCE sign-in, catalogue (search, track, album, artist, playlist, favourites), and stream resolution. 30 unit tests.
  • crates/core/src/tidal_bridge.rs (new): TIDAL's models onto the shared SongItem / BrowseItem / AlbumPage / PlaylistPage shapes, so the client renders TIDAL with no new page code. 7 unit tests.
  • Provider::Tidal, tidal: id helpers, the session blob in AppState, and the playback branch in resolve.
  • Daemon methods for status, sign-in, sign-out, credentials, and the browse surfaces.
  • Client: selector entry, palette colour, providerOf / sourceGlyph, and the account block in Settings and in the account menu. 2 new QML tests.

Two refusals are typed rather than swallowed. tidal_protected is a manifest that carries a content key or declares DASH ContentProtection; tidal_preview is an assetPresentation other than FULL, which is what TIDAL returns when the account is not entitled to the tier requested. Neither is retried, and no key is held, derived or applied anywhere in the crate.

Why

TIDAL was the one streaming service of the four Ryotunes users ask for with no path into the app. The provider seam already carried three catalogues, so the work was a provider crate plus a bridge rather than anything structural.

Surface area

provider, Home, Search, Library, playback, Settings

Validation

  • cargo fmt --all -- --check
  • relevant Rust/frontend tests
  • ./scripts/release-check.sh
  • cd ui && pnpm check && pnpm build
  • affected Ryoku/Hyprland lifecycle tested when applicable

cargo test --workspace --locked passes in full, including ryotunes-tidal 31 and ryotunes-core 150. qmltestrunner -input client/tests 67 passed, though note CI does not run the QML suite, so that figure is a local check rather than an enforced one. scripts/check-release-invariants.py, check-source-shapes.py and check-rust-structure.py all pass.

Regression contracts

None of the listed contracts are touched: no WebKit, MPRIS, DOM, theme-bridge, window-rule or packaging change. The only shared-path edits are the provider branches in methods.rs and the resolve arm, both additive.

Evidence

The crate carries examples/probe.rs, a standalone sign-in and resolve that prints the served quality, codec, sample rate and the refusal reason if there is one:

cargo run -p ryotunes-tidal --example probe -- <track-id> HI_RES_LOSSLESS

tests/live_smoke.rs is #[ignore]d and reads a token from the environment; it checks the profile-name read against a real account without putting a credential in the repository.

Three decisions worth your judgement rather than mine:

A client credential pair IS baked in, and this is the point to argue with. crates/tidal/src/auth.rs ships a device id and secret and a PKCE id and secret as DEFAULT_* constants, so sign-in is one button and the provider works out of the box. TIDAL publishes no credentials for third-party playback, so these come out of TIDAL's own applications, as they do in every other third-party client. That means the repository now carries someone else's credential and the project is bound to it: if TIDAL rotates or revokes the pair, sign-in breaks for everyone until a release ships a new one. A user-supplied pair still overrides the shipped one field by field, so anyone registering their own client keeps using it. If you would rather not carry these, say so and I will take them back out and restore the Settings-only path, at the cost of the provider being inert until the user pastes a pair.

Hi-res needs a second sign-in, and the UI cannot complete it yet. The device flow tops out at LOSSLESS; only a PKCE token is entitled to HI_RES_LOSSLESS, and the entitlement is fixed when the token is minted. Entitlement::ceiling() clamps the request, because asking above it returns a preview rather than an error. The PKCE redirect goes to https://tidal.com/android/login/auth rather than a loopback address, so there is nothing for the client to capture: the crate implements the flow and the UI cannot drive it. Hi-res is therefore reachable from examples/probe.rs and not from the app. Wiring it needs either a loopback redirect on a registered client or a paste-the-URL step, and I did not want to pick one for you.

This delivers hi-res streams, not bit-perfect output. crates/player sets no audio-samplerate, no ao and no exclusive mode, and inserts a float volume filter whenever a track carries a loudness value, so a 24/96 FLAC decodes correctly and is then resampled by the audio server. Changing that is a separate PR and I did not touch it here.

And one thing for you to weigh rather than me. TIDAL's API & SDK overview states that the SDK's Player module is "the only allowed way for third-party applications to incorporate playback of TIDAL content", and that what a third-party application may play through it is previews. This branch does neither: it resolves and plays full streams off the private API, using TIDAL's own application credentials. Every open-source TIDAL client works this way, which is context rather than permission. If that is a line you do not want this project to cross, this is the moment to say so, and the credential question above becomes moot.

Follow-up

  • The PKCE redirect, as above.
  • The tidal glyph is drawn in the same simplified stroke language as the existing spotify and soundcloud entries rather than traced from the trademark, since those two are simplified impressions too. Swap it if you source brand marks differently.

…d the account menu signs in to tidal like it does to spotify
…nt line only once there is an account or a code
…t, a sign-out outlives the exchange it interrupts, a long album gives up all its tracks, and the heart on a tidal track stops going to youtube
@eximora-private

Copy link
Copy Markdown
Author

Done on my end — CI green, release-check passes. Two calls are yours before this goes in: the baked-in client pair, and the playback terms quoted at the bottom of the description. Happy to pull the credentials back out if you’d rather not carry them.

@neur0map
neur0map merged commit cc0781a into Ryoku-dev:main Oct 17, 2026
1 check passed
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