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: object

Main interface for Tidal operations (catalog, auth, download).

Parameters:
  • config (Config | None)

  • token_store (TokenStore | None)

  • media_resolver (Callable[[str], tuple[str, ...]] | None)

  • clock (Callable[[], float] | None)

  • sleeper (Callable[[float], None] | None)

__init__(config=None, *, token_store=None, media_resolver=None, clock=None, sleeper=None)[source]#

Create a client.

Parameters:
  • config (Config | None) – A Config (defaults are safe).

  • token_store (TokenStore | None) – Where the saved login lives. Defaults to a FileTokenStore at config.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 returns None, 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 None without a request.

Parameters:

upc (str) – The album’s UPC / barcode (e.g. 634904078164).

Returns:

The Album model, or None when 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.

Parameters:

album_ids (Sequence[int | str]) – Album ids (legacy numeric or web-app ids).

Returns:

The Album models in request order.

Raises:
Return type:

list[Album]

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 as albums_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.

Parameters:

upcs (Sequence[str]) – Album UPCs / barcodes (e.g. 634904078164).

Returns:

The Album models in request order.

Raises:
Return type:

list[Album]

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 Album dict per album (id, title, release_date, …).

Return type:

list[dict[str, Any]]

artist_top_tracks(url, limit=20)[source]#

The artist’s top tracks, ranked by the catalog.

The v1 artists/{id}/toptracks endpoint (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, as Track models, each with its album back-reference set by the catalog payload.

Parameters:
  • url (str) – An artist link (or bare artist id).

  • limit (int) – 1..50 tracks (the top N — the endpoint is ranked).

Returns:

The ranked tracks (fewer when the catalog has fewer).

Raises:
Return type:

list[Track]

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 None on bulk artists.

Parameters:

artist_ids (Sequence[int | str]) – Artist ids (legacy numeric or web-app ids).

Returns:

The Artist models in request order.

Raises:
Return type:

list[Artist]

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 artwork value search rows and metadata() dicts carry);

  • a resources.tidal.com/images/... URL (the id is parsed out of the path — a host can hand back what artwork_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 None and never raises — cover art is garnish.

Parameters:
  • item (str) – Artwork id, image URL, or Tidal link / bare id.

  • size (int) – Square dimension in pixels, 1..8192.

Returns:

The image bytes (JPEG), or None when unavailable.

Return type:

bytes | None

artwork_url(artwork_id, width=1280, height=1280)[source]#

The static-CDN image URL for a five-segment artwork id (the artwork value 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().

Parameters:
Return type:

str

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: True for a login that the next request would transparently repair.

Returns a JSON-safe dict: logged_in, userId, countryCode, clientId, tokenType, expired, secondsLeft; subscription is added when logged in.

Return type:

dict[str, Any]

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/title plus artist and the catalog garnish when present) — the same row shape search() returns, so a suggest box and a results list render with the same row code, and the rows’ kind/id build links.

Parameters:
  • query (str) – The text being typed.

  • limit (int) – 1..50 suggestions (default 10).

  • country (str | None) – Suggest from this market’s catalog (ISO country code) instead of the account’s — as in search(). (Without a known country the catalog answers empty, not an error.)

Returns:

The suggestion rows ([] for a query with no matches).

Raises:
Return type:

list[dict[str, Any]]

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

close()[source]#

Close the HTTP session (idempotent).

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; False stops the run after the first failure. Both ways the report is returned — inspect report.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, reason plus 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 — one ItemReport per track with status ok/failed/skipped, machine code, and retryable.

Raises:
  • AuthError – not logged in (after a refresh attempt).

  • TidalError – collection-level failures (bad URL, region lock, rate limit, cancellation).

Return type:

DownloadReport

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 failed report carrying the machine code and retryable flag, 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’s url and its position in the input sequence (url_index) — a batch UI correlates events to links without hand-rolling its own bookkeeping.

Cancellation (cancel_event or the client’s stop()) 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:
Returns:

One DownloadReport per processed URL (see the cancellation note above).

Return type:

list[DownloadReport]

finish_device_login(auth, timeout=None, user_id=None)[source]#

Poll until the operator approves (or the deadline passes).

Parameters:
Return type:

TokenRecord

genre_items(genre, item_type='tracks', limit=25, offset=0)[source]#

Browse one genre’s catalog items (the “Discover” tab body).

genre is the genre’s path from genres() (its name works too — they are equal on every catalog genre); item_type one of "tracks", "albums", "playlists", "videos" — the same plural names as the genre row’s has_<item_type> flags, so a browse UI iterates for 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/title plus artist and 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:
  • genre (str) – The genre path (from genres()).

  • item_type (str) – "tracks" / "albums" / "playlists" / "videos".

  • limit (int) – 1..50 items per page.

  • offset (int) – Result offset for paging.

Returns:

{"genre", "type", "total", "items"}.

Raises:
Return type:

dict[str, Any]

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"} plus artwork (the card thumbnail id — pass it to artwork_url()) when the catalog names one. The has_* flags tell a browse UI which tabs a genre card offers: build the card, and for every offered type call genre_items() with the same plural name.

Returns:

One row per genre (the catalog’s fixed list, ~20 genres).

Raises:
Return type:

list[dict[str, Any]]

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 yielded MediaStream is 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_broken is 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 with info.preview is True instead 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_event or the client’s stop()) ends the walk: the generator returns after the in-flight item. An abandoned generator releases the client’s operation registration on close.

Parameters:
  • url (str) – A Tidal link (or bare id): a single track yields one stream; a collection yields one per audio track.

  • quality (Quality | str | None) – As in stream().

  • chunk_size (int | None) – As in stream().

  • cancel_event (Event | None) – As in stream().

  • skip_broken (bool) – Skip unresolvable tracks instead of raising.

  • preview (bool) – As in stream() — accept the ~30-second preview for tracks that have no full stream.

Yields:

One MediaStream per playable track.

Raises:
  • AuthError – not logged in (after a refresh attempt).

  • TidalError – resolution failures (bad URL, unknown resource), and unresolvable tracks when skip_broken is false.

Return type:

Iterator[MediaStream]

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 a failed status and the reason — the library equivalent of the legacy --diagnose --json output. Deep items additionally carry the delivered codec and, when the path can be computed, the expected path plus size (bytes already on disk at that path, 0 when absent) — the host gets a full “what would this download do” report.

Parameters:
  • url (str) – Any Tidal link (or bare id).

  • deep (bool) – resolve each track’s stream (slower, one API call per item) so broken items surface with a reason.

  • quality (Quality | str | None) – Quality profile or label to resolve at in deep mode (default: config.quality).

Return type:

list[ItemReport]

login(access_token, user_id=None)[source]#

Adopt a token the operator pasted in (browser session).

Probes the token against Tidal’s sessions endpoint before persisting it; raises AuthError when rejected.

Parameters:
  • access_token (str)

  • user_id (int | str | None)

Return type:

TokenRecord

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 (False when logged out already or the request failed; the library logs the outcome).

Return type:

bool

lyrics(url)[source]#

The lyrics of a track link, {"lyrics", "subtitles"}.

lyrics is the plain text, subtitles the 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.

Parameters:

url (str)

Return type:

dict[str, str]

metadata(url)[source]#

Metadata for any Tidal link as {"type": ..., "data": {...}}.

The dict mirrors the legacy CLI’s --json output shape, which tidal_transfer’s TidalTask.info JSONField was written against.

Parameters:

url (str)

Return type:

dict[str, Any]

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 subsequent stream() 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 in stream()).

  • preview (bool) – As in stream() (accept the ~30-second preview when the full track is unplayable; True never 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 preview is false, …).

Return type:

dict[str, Any]

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.

Parameters:

playlist_ids (Sequence[int | str]) – Playlist UUIDs (the linkable form).

Returns:

The Playlist models in request order.

Raises:
Return type:

list[Playlist]

resolve(url)[source]#

Resolve any Tidal link (or bare id) to a catalog resource.

Raises:
Parameters:

url (str)

Return type:

Resource

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 contract download_many() applies to downloads. Each row is JSON-safe, one per URL in input order:

  • on success: {"url", "ok": True, "kind", "id", "title", "link"} (kind the lowercase resource kind, id the linkable id, link the canonical URL);

  • on failure: {"url", "ok": False, "code", "reason"} (the machine code for 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_event ends the walk: the remaining URLs are not attempted, and the returned list covers the URLs processed.

Parameters:
Returns:

One row per processed URL, in input order.

Return type:

list[dict[str, Any]]

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"} plus artist for albums and tracks (the same rows the legacy search RPC/CLI emit — the id values build https://tidal.com/<kind>/<id> links straight away; playlist ids are the uuid). Rows additionally carry the catalog’s garnish when present: version (track/album editions), duration in seconds (track/video length, album total), quality (track/album, the raw API value). total is 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’s type is the matching reference-client Type name ("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:

dict[str, Any]

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, plus version/quality/ artwork when known), so a host can render the result exactly like an album-search page.

Parameters:
  • query (str) – Search text (an artist name, an album title, or anything else — the fallback handles it).

  • include_ep (bool | None) – Include EPs/singles in the discography. Defaults to config.include_ep.

Return type:

list[dict[str, Any]]

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 same offset/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, or max_items rows have been collected — the defaults mirror the reference fork’s searchAll (50 per page, 200 rows total).

Result: the same shape as search() ({"text", "type", "total", "items"}), with items the merged rows (page order, groups interleaved) and total the catalog’s overall sum (which may still exceed len(items) when max_items capped the run).

Parameters:
  • query (str) – Search text.

  • item_type (str | None) – As in search() (None = all groups).

  • page_size (int) – Rows per request, 1..50 (the catalog’s cap).

  • max_items (int) – Hard cap on merged rows.

  • country (str | None) – As in search().

Return type:

dict[str, Any]

start_device_login()[source]#

Begin the device flow; show verification_url to a human.

Return type:

DeviceAuth

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). True never downgrades a track that has a full stream; it only turns the PreviewOnlyError into a playable stream with info.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) – MediaStream chunk 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 preview is false, quality unavailable, …).

Return type:

MediaStream

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 returns None, 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 None without a request.

Parameters:

isrc (str) – The track’s ISRC (12 characters, e.g. USUM71801211).

Returns:

The Track model, or None when 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).

Parameters:

url (str)

Return type:

list[dict[str, Any]]

tracks(url)[source]#

The link’s tracks as Track models, 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 album back-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.

Parameters:

track_ids (Sequence[int | str]) – Track ids (legacy numeric or web-app ids).

Returns:

The Track models in request order.

Raises:
Return type:

list[Track]

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 as tracks_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.

Parameters:

isrcs (Sequence[str]) – Track ISRCs (12 characters each, e.g. USUM71801211).

Returns:

The Track models in request order.

Raises:
Return type:

list[Track]

user_playlists(user_id=None, limit=25, offset=0)[source]#

A user’s playlist links (the “My Playlists” tab).

user_id defaults 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 linkable uuid), so a library UI renders them with the same row code as search/browse and builds https://tidal.com/playlist/<uuid> links straight away.

Parameters:
  • user_id (int | str | None) – The user’s id; None (default) is the logged-in session’s user.

  • limit (int) – 1..50 playlists per page.

  • offset (int) – Result offset for paging.

Returns:

{"total", "items"}.

Raises:
  • AuthError – not logged in (after a refresh attempt) — and when no user_id is given (the “self” default needs the session).

  • ConfigError – no user_id given and no session user.

  • TidalError – catalog failures.

Return type:

dict[str, Any]

property cancelled: bool#

True when a cancellation was requested for an active operation.

property logged_in: bool#