Authentication#
tidalpy speaks Tidal’s OAuth2 device flow (no client secret
embedded anywhere in your code) and can also adopt a pasted web
token. The saved session lives in one JSON file (or any custom
TokenStore).
Checking status#
status = client.auth_status()
# {'logged_in': bool, 'userId': ..., 'countryCode': ...,
# 'clientId': ..., 'tokenType': 'device'|'web',
# 'expired': bool, 'secondsLeft': int|None, 'subscription': {...}}
auth_status() never raises for “not logged in” — it returns
logged_in: False. It does try a refresh when the stored token is
expired but a refresh token exists.
Subscription / plan#
When logged in, auth_status() carries a subscription view (the
raw v1 facts plus two computed tier facts, maxQuality and
lossless). For hosts that need to act on the plan,
client.subscription() returns the typed
Subscription model:
sub = client.subscription()
sub.level # "PREMIUM" / "FREE" / ...
sub.max_quality # Quality.LOSSLESS — the highest deliverable profile
sub.allows_lossless # bool (reference-client parity)
sub.allows_quality("24/192") # can this plan deliver Master?
The facts come from the account’s highestSoundQuality ceiling,
and the ceiling is finer than the host-facing quality profiles: an
HI_RES ceiling delivers LOSSLESS (16/44) but not the
HI_RES_LOSSLESS / “Master” (24/192) profile, so
allows_quality("24/192") is False for it. A host pre-checks
deliverability this way before spending a rate-limited resolve a plan
cannot answer. A missing login or a failed lookup degrades to an empty
instance (max_quality is None), never an error.
Device flow#
auth = client.start_device_login()
print("1. Open:", auth.verification_url)
print("2. Enter code:", auth.user_code)
client.finish_device_login(auth, timeout=300) # polls until approved
start_device_login returns a DeviceAuth with the
URL the operator must open, the user code, and a poll deadline.
finish_device_login polls Tidal (interruptibly — setting the
client’s cancellation state stops it) and saves the resulting
TokenRecord on success.
Pasted token#
record = client.login("eyJhbGciOi...")
The token is probed against Tidal’s sessions endpoint before
being persisted; AuthError on rejection.
Token storage#
Default:
FileTokenStoreatconfig.token_file— a single JSON document, written atomically.Custom: implement
TokenStore(load/save/clear) and passtoken_store=...to the client — this is howtidal_transferkeeps sessions in Postgres.
Two rules the library enforces:
A Bearer token is only valid for the OAuth client that issued it — a saved record bound to another client is cleared, not used.
Expiry is tracked (
expires_after); refresh happens transparently via the refresh token, and a failed refresh clears the stale session instead of hammering Tidal.
Logout#
client.logout() # revokes the session remotely, then clears it locally
logout() posts the access token to Tidal’s /v1/logout endpoint
(best-effort — a network failure never blocks the logout) and then
clears the stored session. It returns whether the remote revocation
succeeded, so a host can report “signed out” vs. “signed out locally;
the server-side session may linger”. Only the session that was current
when the logout started is forgotten: if a new login lands while
the revocation is in flight (a host signing a user out on one tab and
back in on another), the newer login survives.
login() rejects a purely numeric “token” up front with an
actionable message — a numeric paste is almost always a user id,
never a Tidal OAuth token, and failing fast saves the probe that
would reject it opaquely.