Client API#
TidalClient — the public entry point of tidalpy.
One TidalClient owns its own HTTP session, OAuth token
lifecycle, and per-instance caches: two clients in one process never
share credentials or state (Django multi-worker / Celery prefork
safe). The client is a facade over the working parts:
Transport— HTTP with Tidal’s policy;OAuth— device flow, refresh, token store;TidalApi— catalog, search, stream resolution;Pipeline— collection downloads;MediaStream— bytes to the caller, no disk.
Typical Django/Celery usage:
client = TidalClient(Config(token_path=..., output=...))
if not client.auth_status()["logged_in"]:
auth = client.start_device_login()
# show auth.verification_url to an operator, then:
client.finish_device_login(auth, timeout=300)
report = client.download(url, quality=Quality.LOSSLESS,
on_progress=lambda e: update_task(e))
for item in report.failures:
log(item.to_dict())
- class tidalpy.client.TidalClient(config=None, *, token_store=None, media_resolver=None, clock=None, sleeper=None)[source]#
Bases:
objectMain interface for Tidal operations (catalog, auth, download).
- Parameters:
- __init__(config=None, *, token_store=None, media_resolver=None, clock=None, sleeper=None)[source]#
Create a client.
- Parameters:
token_store (TokenStore | None) – Where the saved login lives. Defaults to a
FileTokenStoreatconfig.token_file; Django may pass a DB-backed store.media_resolver (Callable[[str], tuple[str, ...]] | None) – Override for media-URL SSRF validation; intended as a test hook (e.g. a local test server). Production code should leave this
None.clock (Callable[[], float] | None) – Monotonic clock override (tests).
sleeper (Callable[[float], None] | None) – Sleep override for the device-login poll (tests); expected to be interruptible.
- Return type:
None
- album_by_upc(upc)[source]#
Resolve an album by its UPC (the indexer facade).
The album counterpart of
track_by_isrc(): a host that holds UPCs from elsewhere (the back of a CD case, a licensing feed) instead of Tidal ids gets the full Album model in one catalog call. A lookup, not a fetch: a UPC the catalog does not know returnsNone, so a library diff is a plain loop with no exception per miss.The input is stripped and otherwise passed through (UPCs run 12-14 digits, no reformatting); blank input returns
Nonewithout a request.- Parameters:
upc (str) – The album’s UPC / barcode (e.g.
634904078164).- Returns:
The Album model, or
Nonewhen the catalog has no album with that barcode.- Raises:
AuthError – not logged in (after a refresh attempt).
TidalError – catalog failures (a malformed value is the catalog’s 400, which propagates).
- Return type:
Album | None
- albums_by_ids(album_ids)[source]#
Bulk album metadata (the indexer facade).
The album counterpart of
tracks_by_ids()(an indexer importing a discography from ids): full Album models in one catalog round trip per 50 ids. Same contract — request order, duplicates collapse, unknown ids skipped not fabricated (diff to find the gaps).The v2 bulk rows carry no cover art (the single-album resource does), so bulk albums have no artwork.
- albums_by_upcs(upcs)[source]#
Bulk album metadata by UPC (the indexer facade).
The bulk sibling of
album_by_upc(): a host that holds a library of barcodes from elsewhere (a shelf of CD cases, a licensing feed) instead of Tidal ids gets the full Album models in one catalog round trip per 50 UPCs, instead of N single lookups. Same contract asalbums_by_ids(): request order, duplicates collapse, barcodes the catalog does not know are skipped, not fabricated (diff to find the gaps).Each UPC is stripped and otherwise passed through (no reformatting); blank entries are skipped.
- artist_albums(url, include_ep=None)[source]#
The discography of an artist link (or bare artist id).
- Parameters:
url (str) –
https://tidal.com/artist/<id>or a bare id.include_ep (bool | None) – also include EPs and singles (a second, filtered listing). Defaults to
include_ep(on, matching the reference client’s effective default).
- Returns:
One
Albumdict per album (id, title, release_date, …).- Return type:
- artist_top_tracks(url, limit=20)[source]#
The artist’s top tracks, ranked by the catalog.
The v1
artists/{id}/toptracksendpoint (live API verified: paged, full track payloads — the reference clients don’t expose it). A real-time “top tracks” tab for an artist page: the tracks come back ranked, asTrackmodels, each with its album back-reference set by the catalog payload.- Parameters:
- Returns:
The ranked tracks (fewer when the catalog has fewer).
- Raises:
AuthError – not logged in.
TidalError – resolution failures (bad URL, unknown artist).
- Return type:
- artists_by_ids(artist_ids)[source]#
Bulk artist metadata (the indexer facade).
The artist counterpart of
tracks_by_ids()— also the only v2 artist resolution path (the single artist fetch is v1), so web-app ids the v1 catalog does not know are resolvable here. Same contract: request order, duplicates collapse, unknown ids skipped not fabricated (diff to find the gaps).The v2 bulk rows carry no picture/handle, so those stay
Noneon bulk artists.
- artwork(item, size=1280)[source]#
The cover art for a link, row artwork id, or image URL.
Accepts, in order of preference:
a five-segment artwork id (the
artworkvalue search rows andmetadata()dicts carry);a
resources.tidal.com/images/...URL (the id is parsed out of the path — a host can hand back whatartwork_url()built);any Tidal link (or bare id): it is resolved and the artwork id of the resource (album cover, playlist cover, artist picture, track’s album cover) is used.
The fetch goes through the SSRF-validated transport (the same validated-redirect-follow the download pipeline uses), so a host can serve these bytes without doing its own HTTP. Best effort: a failed fetch (no artwork, dead CDN, refused host) returns
Noneand never raises — cover art is garnish.
- artwork_url(artwork_id, width=1280, height=1280)[source]#
The static-CDN image URL for a five-segment artwork id (the
artworkvalue of search rows,Album.artwork_id).Pure URL construction — no network, no auth: search and metadata clients render thumbnails straight from the ids the catalog returns. See
tidalpy.artwork_url().
- auth_status()[source]#
Current login state (plus subscription facts when logged in).
Verifies the login the way a download would use it: the subscription call goes through the transport’s 401 handshake, so an expired access token with a valid refresh token is refreshed before the status is reported — the host never sees
expired: Truefor a login that the next request would transparently repair.Returns a JSON-safe dict:
logged_in,userId,countryCode,clientId,tokenType,expired,secondsLeft;subscriptionis added when logged in.
- autocomplete(query, limit=10, country=None)[source]#
Catalog autocomplete — the search-box suggest for a real-time search UI.
The keystroke endpoint: call it as the user types (it answers with a small result budget, across artists, albums, tracks, and playlists). Rows are the uniform search row (
kind/id/titleplusartistand the catalog garnish when present) — the same row shapesearch()returns, so a suggest box and a results list render with the same row code, and the rows’kind/idbuild links.- Parameters:
- Returns:
The suggestion rows (
[]for a query with no matches).- Raises:
AuthError – not logged in (after a refresh attempt).
TidalError – transport failures.
- Return type:
- cancel()[source]#
Cancel every in-flight operation on this client.
Cancellation is cooperative: current requests finish their chunk and the next check raises
CancelledError.- Return type:
None
- download(url, dest=None, quality=None, skip_broken=None, on_progress=None, cancel_event=None)[source]#
Download a track, album, playlist, mix, or artist.
- Parameters:
url (str) – Any Tidal link (or bare id).
dest (str | Path | None) – Destination directory override (default:
config.output_dir).quality (Quality | str | None) – Quality profile or label (
"master","flac"); default:config.quality.skip_broken (bool | None) –
True(default) skips broken items and keeps going;Falsestops the run after the first failure. Both ways the report is returned — inspectreport.failures.on_progress (Callable[[dict[str, Any]], None] | None) – Callback receiving plain JSON-safe dicts:
stage(start/item/bytes/complete),kind,index,total,id,title,status,reasonplus extras (bytes,total_bytes,path). Callback exceptions are logged, never propagated.cancel_event (Event | None) – Optional host-owned event; setting it cancels. (
client.cancel()cancels all active operations.)
- Returns:
A
DownloadReport— oneItemReportper track with statusok/failed/skipped, machinecode, andretryable.- Raises:
AuthError – not logged in (after a refresh attempt).
TidalError – collection-level failures (bad URL, region lock, rate limit, cancellation).
- Return type:
- download_many(urls, quality=None, skip_broken=True, on_progress=None, cancel_event=None)[source]#
Download many links in one call (the indexing-client facade).
Hosts that collect links in a loop (a search-results importer, a watchlist sync) would otherwise repeat the resolve + pipeline dance per URL and hand-roll their own failure bookkeeping. This walks the links with the library’s contracts:
Per-URL failure isolation: a bad link (unresolvable, region locked, rate-limited out, a collection that fails before its first item) becomes a single-item
failedreport carrying the machinecodeandretryableflag, and the walk continues.One report per URL, in input order, so the host correlates results to links by position.
Sequential runs: each run already manages its own quality downgrade, retries, and the server’s rate-limit budget; parallel collections would only fight over that budget.
Progress events are the per-run dicts, in order (the flat event contract of
download()), each annotated with the run’surland its position in the input sequence (url_index) — a batch UI correlates events to links without hand-rolling its own bookkeeping.
Cancellation (
cancel_eventor the client’sstop()) ends the walk: the run in flight returns its (cancelled- marked) report, and the remaining URLs are not attempted — the returned list covers only the URLs processed.- Parameters:
urls (Sequence[str]) – Tidal links (or bare ids), in download order.
quality (Quality | str | None) – As in
download().skip_broken (bool) – As in
download().on_progress (Callable[[dict[str, Any]], None] | None) – As in
download().cancel_event (Event | None) – As in
download().
- Returns:
One
DownloadReportper processed URL (see the cancellation note above).- Return type:
- finish_device_login(auth, timeout=None, user_id=None)[source]#
Poll until the operator approves (or the deadline passes).
- Parameters:
auth (DeviceAuth)
timeout (float | None)
- Return type:
- genre_items(genre, item_type='tracks', limit=25, offset=0)[source]#
Browse one genre’s catalog items (the “Discover” tab body).
genreis the genre’spathfromgenres()(itsnameworks too — they are equal on every catalog genre);item_typeone of"tracks","albums","playlists","videos"— the same plural names as the genre row’shas_<item_type>flags, so a browse UI iteratesfor item_type in ("tracks", "albums", "playlists", "videos"): if genre["has_" + item_type]: ....Result:
{"genre", "type", "total", "items"}where each item is the uniform search row (kind/id/titleplusartistand the catalog garnish when present) — a browse tab renders with the same row code as a search result, and the rows build links straight away.- Parameters:
- Returns:
{"genre", "type", "total", "items"}.- Raises:
AuthError – not logged in (after a refresh attempt).
ConfigError – unknown
item_type.NotFoundError – unknown genre.
- Return type:
- genres()[source]#
The catalog’s browse genres (the web app’s “Discover” grid).
Each row is
{"name", "path", "has_playlists", "has_artists", "has_albums", "has_tracks", "has_videos"}plusartwork(the card thumbnail id — pass it toartwork_url()) when the catalog names one. Thehas_*flags tell a browse UI which tabs a genre card offers: build the card, and for every offered type callgenre_items()with the same plural name.
- iter_streams(url, quality=None, chunk_size=None, cancel_event=None, skip_broken=True, preview=False)[source]#
Readable streams for a link’s tracks, in catalog order.
The player-client facade: the
stream()equivalent for collections (an album, playlist, mix, artist discography) — a host that plays a collection in order (a UI, a gapless player, a live broadcast) without saving anything. Each yieldedMediaStreamis independent: pull its chunks (or open it as a context manager) and close it before relying on the next one.Tracks come back in the collection’s order; non-audio slots (music videos) are skipped — an audio-only library.
Per-track failure isolation: an unresolvable track (broken item, region lock, quality unavailable) is logged and skipped when
skip_brokenis true; with it false the error propagates to the host.preview=True(the “play what you can” path): tracks the catalog can only serve as a ~30-second preview (free-tier accounts, pre-release/exclusive tracks) yield a preview stream withinfo.preview is Trueinstead of counting as broken — the player plays something in every slot and can show a badge. Full streams are still preferred; the flag never downgrades a playable track.Streams are resolved one at a time, lazily, as the host asks for them — the server’s rate-limit budget is shared with every other operation on this client.
Cancellation (
cancel_eventor the client’sstop()) ends the walk: the generator returns after the in-flight item. An abandoned generator releases the client’s operation registration on close.
- Parameters:
- Yields:
One
MediaStreamper playable track.- Raises:
AuthError – not logged in (after a refresh attempt).
TidalError – resolution failures (bad URL, unknown resource), and unresolvable tracks when
skip_brokenis false.
- Return type:
- list_items(url, deep=False, quality=None)[source]#
Item report for a collection (or single track).
Shallow: catalog facts only (every track item is
ok). Deep: each track’s stream is actually resolved, so broken items show up with afailedstatus and the reason — the library equivalent of the legacy--diagnose --jsonoutput. Deep items additionally carry the deliveredcodecand, when the path can be computed, the expectedpathplussize(bytes already on disk at that path,0when absent) — the host gets a full “what would this download do” report.- Parameters:
- Return type:
- login(access_token, user_id=None)[source]#
Adopt a token the operator pasted in (browser session).
Probes the token against Tidal’s
sessionsendpoint before persisting it; raisesAuthErrorwhen rejected.- Parameters:
- Return type:
- logout()[source]#
End the session: best-effort remote revocation (Tidal’s
/v1/logout), then forget the saved session locally.A failed revocation never blocks the logout — the local session is cleared either way. Returns whether the remote revocation succeeded (
Falsewhen logged out already or the request failed; the library logs the outcome).- Return type:
- lyrics(url)[source]#
The lyrics of a track link,
{"lyrics", "subtitles"}.lyricsis the plain text,subtitlesthe timed (LRC) version; each is""when Tidal has none (lyrics never error — a track without them simply returns both empty). Requires a single track link.When Tidal has plain lyrics but no timed subtitles for this exact version (a remaster, a live edition), a fallback searches the catalog for a matching version and adopts its timed subtitles (keeping this track’s plain text) — so display clients get synced lyrics for the versions Tidal tags up as well.
- metadata(url)[source]#
Metadata for any Tidal link as
{"type": ..., "data": {...}}.The dict mirrors the legacy CLI’s
--jsonoutput shape, whichtidal_transfer’sTidalTask.infoJSONField was written against.
- playback(url, quality=None, preview=False)[source]#
What will actually be played (the player facade).
Resolves the track’s stream without downloading a byte: the delivered quality, codec, rate/depth, preview flag, and the expiring CDN link (
url) for hosts that fetch the bytes themselves. A player UI shows the quality badge (“192 kHz / 24-bit FLAC”) before pressing play; a sync host pre-checks playability. The resolution is cached per (account, track, quality, preview) — a subsequentstream()of the same track at the same quality spends no second rate-limited probe.- Parameters:
url (str) – A Tidal track link (or bare track id).
quality (Quality | str | None) – Quality profile or label; default:
config.quality(as instream()).preview (bool) – As in
stream()(accept the ~30-second preview when the full track is unplayable;Truenever downgrades a track that has a full stream).
- Returns:
The resolved stream as a plain JSON-safe dict (
StreamInfo.to_dict):trackId,url,segments,codec,deliveredQuality,requestedQuality,bandwidth,sampleRate,bitDepth,channels,preview, …- Raises:
AuthError – not logged in (after a refresh attempt).
TidalError – resolution failures (bad URL, not a track, preview-only when
previewis false, …).
- Return type:
- playlists_by_ids(playlist_ids)[source]#
Bulk playlist metadata (the indexer facade).
The playlist counterpart of
tracks_by_ids(): a host that has playlist UUIDs from elsewhere (a web-app export, a cross-device library sync) gets the full Playlist models in one catalog round trip per 50 UUIDs, instead of N resolves. Same contract: request order, duplicates collapse, unknown UUIDs skipped not fabricated (diff to find the gaps).The rows carry the card facts (title, description, track count, duration, follower count) but no artwork: a host that needs a cover resolves that playlist singly.
- resolve(url)[source]#
Resolve any Tidal link (or bare id) to a catalog resource.
- Raises:
NotFoundError – unknown id or empty URL.
UnsupportedType – video links (audio-only library).
- Parameters:
url (str)
- Return type:
- resolve_many(urls, cancel_event=None)[source]#
Resolve many links (or bare ids) with per-item isolation — the indexer’s pre-flight before spending a download on the batch.
A pasted/exports blob (
parse_link_list) carries the odd dead id, a video link, or a typo; the walk must not die on one bad row — the same isolation contractdownload_many()applies to downloads. Each row is JSON-safe, one per URL in input order:on success:
{"url", "ok": True, "kind", "id", "title", "link"}(kindthe lowercase resource kind,idthe linkable id,linkthe canonical URL);on failure:
{"url", "ok": False, "code", "reason"}(the machinecodefor library errors —"not-found","unsupported-type", … — the exception’s name otherwise).
The walk is sequential (resolutions are catalog-light; parallel calls would only fight over the rate-limit budget).
cancel_eventends the walk: the remaining URLs are not attempted, and the returned list covers the URLs processed.
- search(query, item_type=None, limit=20, offset=0, country=None)[source]#
Search the catalog; returns the reference client’s search shape.
Result:
{"text", "type", "total", "items"}where each item is{"kind", "id", "title"}plusartistfor albums and tracks (the same rows the legacysearchRPC/CLI emit — theidvalues buildhttps://tidal.com/<kind>/<id>links straight away; playlist ids are theuuid). Rows additionally carry the catalog’s garnish when present:version(track/album editions),durationin seconds (track/video length, album total),quality(track/album, the raw API value).totalis the catalog’s per-group sum, so it may exceed the number of rows returned.- Parameters:
query (str) – Search text.
item_type (str | None) –
"track"/"album"/"artist"/"playlist"/"video";None(default) searches all five groups (reference-client parity). The result’stypeis the matching reference-clientTypename ("Null"for the default all-groups search).limit (int) – 1..50 results per group.
offset (int) – Result offset for paging through the catalog.
country (str | None) – Search this market’s catalog (ISO country code, e.g.
"US") instead of the account’s — a search UI serving an account in another region.None(default) uses the session’s country.
- Return type:
- search_albums(query, include_ep=None)[source]#
Album search that understands artist-name queries.
The catalog ranks album search by title, so searching for an artist’s name (“Radiohead”) returns only the albums whose titles match — the artist’s actual discography is mostly hidden. This method fixes that: when the query (case and punctuation-insensitively) is the name of a catalog artist, that artist’s full discography is fetched and listed first, followed by the title-matching albums, deduplicated by id. When no artist matches the name, it falls back to the paged album search (
search_all(), album group).Rows are the uniform
search()album shape (kind,id,title,artist, plusversion/quality/artworkwhen known), so a host can render the result exactly like an album-search page.
- search_all(query, item_type=None, page_size=50, max_items=200, country=None)[source]#
Page through the catalog search until it is exhausted.
search()returns one page per group; this walks the pages (the catalog pages each group by the sameoffset/limit) and merges the rows, so an indexing client can collect all matches of a query without paging the host. It stops when every group is exhausted (the per-group totals), a page comes back empty, a page is short, ormax_itemsrows have been collected — the defaults mirror the reference fork’ssearchAll(50 per page, 200 rows total).Result: the same shape as
search()({"text", "type", "total", "items"}), withitemsthe merged rows (page order, groups interleaved) andtotalthe catalog’s overall sum (which may still exceedlen(items)whenmax_itemscapped the run).
- stream(url, quality=None, preview=False, cancel_event=None, chunk_size=None)[source]#
Resolve a track link to a readable plaintext byte stream.
No disk I/O happens in the library: the host pulls chunks and writes them wherever it wants (S3 multipart, DB blob, pipe to ffmpeg). For collections, use
iter_streams()(one stream per audio track, in order) or resolve a track link first (list_items/metadata).- Parameters:
url (str) – A Tidal track link (or bare track id).
quality (Quality | str | None) – Quality profile or label; default:
config.quality.preview (bool) – Accept the ~30-second preview when the catalog cannot deliver the full track (free-tier accounts, pre-release/exclusive tracks — even on subscribed accounts).
Truenever downgrades a track that has a full stream; it only turns thePreviewOnlyErrorinto a playable stream withinfo.preview is True(the search-UI “play something” path). Downloads never resolve previews: a 30-second file is never a valid download result.cancel_event (Event | None) – Optional host-owned cancellation event.
chunk_size (int | None) –
MediaStreamchunk size (>= 16 KiB); default:DEFAULT_CHUNK_SIZE(256 KiB).
- Returns:
A
MediaStream(context manager, iterator of bytes,.size,.info,.track).- Raises:
AuthError – not logged in.
TidalError – resolution failures (bad URL, preview-only when
previewis false, quality unavailable, …).
- Return type:
- track_by_isrc(isrc)[source]#
Resolve a track by its ISRC (the indexer facade).
The key-based counterpart of
tracks_by_ids(): a host that holds ISRCs from elsewhere (CDDB, a licensing feed, another catalog’s export) instead of Tidal ids gets the full Track model in one catalog call. A lookup, not a fetch: an ISRC the catalog does not know returnsNone, so a library diff is a plain loop with no exception per miss.The input is normalised (whitespace removed, upper-cased — the catalog rejects any other case); blank input returns
Nonewithout a request.- Parameters:
isrc (str) – The track’s ISRC (12 characters, e.g.
USUM71801211).- Returns:
The Track model, or
Nonewhen the catalog has no track with that ISRC.- Raises:
AuthError – not logged in (after a refresh attempt).
TidalError – catalog failures (a malformed ISRC is the catalog’s 400, which propagates).
- Return type:
Track | None
- track_contributors(url)[source]#
The credits of a track link (the track page’s “credits” tab).
Each row is
{"name", "role"}(the catalog’s credit strings: Producer, Composer, Lyricist, Associated Performer, Music Publisher, …). A track with no credits returns[](a legitimate empty credit list, not an error); a link that is not a resolvable track raises like the other track facades. Requires a single track link (or bare track id).
- tracks(url)[source]#
The link’s tracks as
Trackmodels, in catalog order.The content-listing facade for real-time UIs and indexers: an album page (title, number, duration, explicit, bpm/key per row), a playlist page, an artist’s flattened discography (each track carrying its
albumback-reference), or a single track (one-element list). The listing is the catalog’s — no stream is resolved, so it is cheap and rate-limit-light (one items call per collection, cached per client instance).Non-audio slots (music videos) are skipped; video links are not supported by this library and raise as in
resolve().- Parameters:
url (str) – Any Tidal link (or bare id) this library resolves: album, playlist, mix, artist, or single track.
- Returns:
track number; playlist/mix: playlist position; artist: discography order, per-album).
- Return type:
The tracks in catalog order (album
- Raises:
AuthError – not logged in.
TidalError – resolution failures (bad URL, unknown resource); video links (UnsupportedType).
- tracks_by_ids(track_ids)[source]#
Bulk track metadata (the indexer facade).
The ids-first counterpart of
tracks(): a host that has track ids from elsewhere (a playlist export, a library audit, a web-app deep link) gets the full Track models in one catalog round trip per 50 ids, instead of N single-track resolves — each of which starts as a rate-limited fetch.Result order is the request order (duplicates collapse to the first occurrence); ids the catalog does not know are skipped, not fabricated — diff the result against the request to find the gaps.
- tracks_by_isrcs(isrcs)[source]#
Bulk track metadata by ISRC (the indexer facade).
The bulk sibling of
track_by_isrc(): a host that holds a library of ISRCs from elsewhere (a CDDB export, a licensing feed) instead of Tidal ids gets the full Track models in one catalog round trip per 50 ISRCs, instead of N single lookups. Same contract astracks_by_ids(): request order, duplicates collapse, ISRCs the catalog does not know are skipped, not fabricated (diff to find the gaps).Each ISRC is normalised (whitespace removed, upper-cased); blank entries are skipped.
- user_playlists(user_id=None, limit=25, offset=0)[source]#
A user’s playlist links (the “My Playlists” tab).
user_iddefaults to the logged-in session’s user (the account’s own playlists); pass another user id for that user’s public playlists. Rows are the uniform playlist search rows (kind/id/title— the id is the linkableuuid), so a library UI renders them with the same row code as search/browse and buildshttps://tidal.com/playlist/<uuid>links straight away.- Parameters:
- Returns:
{"total", "items"}.- Raises:
AuthError – not logged in (after a refresh attempt) — and when no
user_idis given (the “self” default needs the session).ConfigError – no
user_idgiven and no session user.TidalError – catalog failures.
- Return type: