Source code for minim.tidal

"""
TIDAL
=====
.. moduleauthor:: Benjamin Ye <GitHub: bbye98>

This module contains a complete implementation of all public TIDAL API
endpoints and a minimum implementation of the more robust but private
TIDAL API.
"""

import base64
import datetime
import hashlib
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import logging
from multiprocessing import Process
import os
import pathlib
import re
import secrets
import time
from typing import Any, Union
import urllib
import warnings
import webbrowser
from xml.dom import minidom

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import requests

from . import FOUND_FLASK, FOUND_PLAYWRIGHT, DIR_HOME, DIR_TEMP, _config

if FOUND_FLASK:
    from flask import Flask, request
if FOUND_PLAYWRIGHT:
    from playwright.sync_api import sync_playwright

__all__ = ["API", "PrivateAPI"]


class _TIDALRedirectHandler(BaseHTTPRequestHandler):
    """
    HTTP request handler for the TIDAL authorization code flow.
    """

    def do_GET(self):
        """
        Handles an incoming GET request and parses the query string.
        """
        self.server.response = dict(
            urllib.parse.parse_qsl(urllib.parse.urlparse(f"{self.path}").query)
        )
        self.send_response(200)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        status = "denied" if "error" in self.server.response else "granted"
        self.wfile.write(
            f"Access {status}. You may close this page now.".encode()
        )


[docs] class API: """ TIDAL API client. The TIDAL API exposes TIDAL functionality and data, making it possible to build applications that can search for and retrieve metadata from the TIDAL catalog. .. seealso:: For more information, see the `TIDAL API Reference <https://developer.tidal.com/apiref>`_. Requests to the TIDAL API endpoints must be accompanied by a valid access token in the header. An access token can be obtained with or without user authentication. While authentication is not necessary to search for and retrieve data from public content, it is required to access personal content and control playback. Minim can obtain client-only access tokens via the client credentials flow and user access tokens via the authorization code with proof key for code exchange (PKCE) flow. These OAuth 2.0 authorization flows require valid client credentials (client ID and client secret) to either be provided to this class's constructor as keyword arguments or be stored as :code:`TIDAL_CLIENT_ID` and :code:`TIDAL_CLIENT_SECRET` in the operating system's environment variables. .. seealso:: To get client credentials, see the `guide on how to register a new TIDAL application <https://developer.tidal.com/documentation /dashboard/dashboard-client-credentials>`_. If an existing access token is available, it and its accompanying information (refresh token and expiry time) can be provided to this class's constructor as keyword arguments to bypass the access token retrieval process. It is recommended that all other authorization-related keyword arguments be specified so that a new access token can be obtained when the existing one expires. .. tip:: The authorization flow and access token can be changed or updated at any time using :meth:`set_flow` and :meth:`set_access_token`, respectively. Minim also stores and manages access tokens and their properties. When an access token is acquired, it is automatically saved to the Minim configuration file to be loaded on the next instantiation of this class. This behavior can be disabled if there are any security concerns, like if the computer being used is a shared device. Parameters ---------- client_id : `str`, keyword-only, optional Client ID. Required for the client credentials flow. If it is not stored as :code:`TIDAL_CLIENT_ID` in the operating system's environment variables or found in the Minim configuration file, it must be provided here. client_secret : `str`, keyword-only, optional Client secret. Required for the client credentials flow. If it is not stored as :code:`TIDAL_CLIENT_SECRET` in the operating system's environment variables or found in the Minim configuration file, it must be provided here. flow : `str`, keyword-only, optional Authorization flow. .. container:: **Valid values**: * :code:`"client_credentials"` for the client credentials flow. * :code:`"pkce"` for the authorization code with proof key for code exchange (PKCE) flow. browser : `bool`, keyword-only, default: :code:`False` Determines whether a web browser is automatically opened for the authorization code (with PKCE) flow. If :code:`False`, users will have to manually open the authorization URL. Not applicable when `web_framework="playwright"`. web_framework : `str`, keyword-only, optional Determines which web framework to use for the authorization code (with PKCE) flow. .. container:: **Valid values**: * :code:`"http.server"` for the built-in implementation of HTTP servers. * :code:`"flask"` for the Flask framework. * :code:`"playwright"` for the Playwright framework by Microsoft. port : `int` or `str`, keyword-only, default: :code:`8888` Port on :code:`localhost` to use for the authorization code flow with the :code:`http.server` and Flask frameworks. Only used if `redirect_uri` is not specified. redirect_uri : `str`, keyword-only, optional Redirect URI for the authorization code flow. If not on :code:`localhost`, the automatic authorization code retrieval functionality is not available. scopes : `str` or `list`, keyword-only, optional Authorization scopes to request user access for in the authorization code flow. .. seealso:: See :meth:`get_scopes` for the complete list of scopes. access_token : `str`, keyword-only, optional Access token. If provided here or found in the Minim configuration file, the authorization process is bypassed. In the former case, all other relevant keyword arguments should be specified to automatically refresh the access token when it expires. expiry : `datetime.datetime` or `str`, keyword-only, optional Expiry time of `access_token` in the ISO 8601 format :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be reauthenticated using the specified authorization flow (if possible) when `access_token` expires. overwrite : `bool`, keyword-only, default: :code:`False` Determines whether to overwrite an existing access token in the Minim configuration file. save : `bool`, keyword-only, default: :code:`True` Determines whether newly obtained access tokens and their associated properties are stored to the Minim configuration file. Attributes ---------- session : `requests.Session` Session used to send requests to the TIDAL API. API_URL : `str` Base URL for the TIDAL API. AUTH_URL : `str` URL for TIDAL API authorization code requests. TOKEN_URL : `str` URL for the TIDAL API token endpoint. """ _FLOWS = {"client_credentials", "pkce"} _NAME = f"{__module__}.{__qualname__}" _VERSION = "0.1.99" API_URL = "https://openapi.tidal.com/v2" AUTH_URL = "https://login.tidal.com/authorize" TOKEN_URL = "https://auth.tidal.com/v1/oauth2/token"
[docs] @classmethod def get_scopes(self, categories: Union[str, list[str]]) -> str: """ Get TIDAL API authorization scopes for the specified categories. Parameters ---------- categories : `str` or `list` Categories of authorization scopes to get. .. container:: **Valid values**: * :code:`"collections"` for scopes related to user collections, such as :code:`collection.read` and :code:`collection.write`. * :code:`"entitlements"` for scopes related to user entitlements, such as :code:`entitlements.read`. * :code:`"playback"` for scopes related to playback control, such as :code:`playback`. * :code:`"playlists"` for scopes related to playlists, such as :code:`playlists.read` and :code:`playlists.write`. * :code:`"recommendations"` for scopes related to user recommendations, such as :code:`recommendations.read`. * :code:`"search"` for scopes related to search functionality, such as :code:`search.read` and :code:`search.write`. * :code:`"user"` for scopes related to user information, such as :code:`user.read`. * :code:`"all"` for all scopes above. * A substring to match in the possible scopes, such as * :code:`"read"` for all scopes above that grant read access, i.e., scopes with :code:`read` in the name, or * :code:`"write"` for all scopes above that grant write access, i.e., scopes with :code:`write` in the name. .. seealso:: For the endpoints that the scopes allow access to, see the `TIDAL API Reference <https://tidal-music.github.io/tidal-api-reference/>`_. """ SCOPES = { "collection": ["collection.read", "collection.write"], "entitlements": ["entitlements.read"], "playback": ["playback"], "playlists": ["playlists.read", "playlists.write"], "recommendations": ["recommendations.read"], "search": ["search.read", "search.write"], "user": ["user.read"], } if isinstance(categories, str): if categories in SCOPES.keys(): return SCOPES[categories] if categories == "all": return " ".join( s for scopes in SCOPES.values() for s in scopes ) return " ".join( s for scopes in SCOPES.values() for s in scopes if categories in s ) return " ".join( s for scopes in (self.get_scopes[c] for c in categories) for s in scopes )
def __init__( self, *, client_id: str = None, client_secret: str = None, flow: str = "client_credentials", browser: bool = False, web_framework: str = None, port: Union[int, str] = 8888, redirect_uri: str = None, scopes: Union[str, list[str]] = "", access_token: str = None, refresh_token: str = None, expiry: Union[datetime.datetime, str] = None, overwrite: bool = False, save: bool = True, ) -> None: """ Create a TIDAL API client. """ self.session = requests.Session() self.session.headers["accept"] = self.session.headers[ "Content-Type" ] = "application/vnd.api+json" if ( access_token is None and _config.has_section(self._NAME) and not overwrite ): flow = _config.get(self._NAME, "flow") access_token = _config.get(self._NAME, "access_token") refresh_token = _config.get( self._NAME, "refresh_token", fallback=None ) expiry = _config.get(self._NAME, "expiry") client_id = _config.get(self._NAME, "client_id") client_secret = _config.get( self._NAME, "client_secret", fallback=None ) redirect_uri = _config.get( self._NAME, "redirect_uri", fallback=None ) scopes = _config.get(self._NAME, "scopes") self.set_flow( flow, client_id=client_id, client_secret=client_secret, browser=browser, web_framework=web_framework, port=port, redirect_uri=redirect_uri, scopes=scopes, save=save, ) self.set_access_token( access_token, refresh_token=refresh_token, expiry=expiry ) def _check_authentication(self, endpoint: str) -> None: """ Check if the user is authenticated for the desired endpoint. Parameters ---------- endpoint : `str` TIDAL API endpoint. """ if self._flow != "pkce": emsg = f"{self._NAME}.{endpoint}() requires user authentication." raise RuntimeError(emsg) def _check_scope(self, endpoint: str, scope: str) -> None: """ Check if the user has granted the appropriate authorization scope for the desired endpoint. Parameters ---------- endpoint : `str` TIDAL API endpoint. scope : `str` Required scope for `endpoint`. """ if scope not in self._scopes: emsg = ( f"{self._NAME}.{endpoint}() requires the '{scope}' " "authorization scope." ) raise RuntimeError(emsg) def _get_authorization_code(self, code_challenge: str = None) -> str: """ Get an authorization code to be exchanged for an access token in the authorization code flow. Parameters ---------- code_challenge : `str`, optional Code challenge for the authorization code with PKCE flow. Returns ------- auth_code : `str` Authorization code. """ params = { "client_id": self._client_id, "redirect_uri": self._redirect_uri, "response_type": "code", "state": secrets.token_urlsafe(), } if self._scopes: params["scope"] = self._scopes if code_challenge is not None: params["code_challenge"] = code_challenge params["code_challenge_method"] = "S256" auth_url = f"{self.AUTH_URL}?{urllib.parse.urlencode(params)}" if self._web_framework == "playwright": har_file = DIR_TEMP / "minim_tidal.har" with sync_playwright() as playwright: browser = playwright.firefox.launch(headless=False) context = browser.new_context(record_har_path=har_file) page = context.new_page() page.goto(auth_url, timeout=0) with page.expect_request( "https://login.tidal.com/*/authorize/accept*" ) as _: pass # blocking call context.close() browser.close() with open(har_file, "r") as f: queries = dict( urllib.parse.parse_qsl( urllib.parse.urlparse( re.search( rf'{self._redirect_uri}\?(.*?)"', f.read() ).group(0) ).query ) ) har_file.unlink() else: if self._browser: webbrowser.open(auth_url) else: print( "To grant Minim access to TIDAL data and " "features, open the following link in your web " f"browser:\n\n{auth_url}\n" ) if self._web_framework == "http.server": httpd = HTTPServer(("", self._port), _TIDALRedirectHandler) httpd.handle_request() queries = httpd.response elif self._web_framework == "flask": app = Flask(__name__) json_file = DIR_TEMP / "minim_tidal.json" @app.route("/callback", methods=["GET"]) def _callback() -> str: if "error" in request.args: return "Access denied. You may close this page now." with open(json_file, "w") as f: json.dump(request.args, f) return "Access granted. You may close this page now." server = Process(target=app.run, args=("0.0.0.0", self._port)) server.start() while not json_file.is_file(): time.sleep(0.1) server.terminate() with open(json_file, "rb") as f: queries = json.load(f) json_file.unlink() else: uri = input( "After authorizing Minim to access TIDAL on " "your behalf, copy and paste the URI beginning " f"with '{self._redirect_uri}' below.\n\nURI: " ) queries = dict( urllib.parse.parse_qsl(urllib.parse.urlparse(uri).query) ) if "error" in queries: raise RuntimeError( f"Authorization failed. Error: {queries['error']}" ) if params["state"] != queries["state"]: raise RuntimeError("Authorization failed due to state mismatch.") return queries["code"] def _get_json(self, url: str, **kwargs) -> dict: """ Send a GET request and return the JSON-encoded content of the response. Parameters ---------- url : `str` URL for the GET request. **kwargs Keyword arguments to pass to :meth:`requests.request`. Returns ------- resp : `dict` JSON-encoded content of the response. """ return self._request("get", url, **kwargs).json() def _refresh_access_token(self) -> None: """ Refresh the expired excess token. """ if self._flow == "client_credentials": self.set_access_token() else: client_b64 = base64.urlsafe_b64encode( f"{self._client_id}:{self._client_secret}".encode() ).decode() r = requests.post( self.TOKEN_URL, data={ "grant_type": "refresh_token", "refresh_token": self._refresh_token, }, headers={"Authorization": f"Basic {client_b64}"}, ).json() self.session.headers["Authorization"] = ( f"Bearer {r['access_token']}" ) self._expiry = datetime.datetime.now() + datetime.timedelta( 0, r["expires_in"] ) self._scopes = r["scope"] if self._save: _config[self._NAME].update( { "access_token": r["access_token"], "refresh_token": self._refresh_token, "expiry": self._expiry.strftime("%Y-%m-%dT%H:%M:%SZ"), "scopes": self._scopes, } ) with open(DIR_HOME / "minim.cfg", "w") as f: _config.write(f) def _request(self, method: str, url: str, **kwargs) -> requests.Response: """ Construct and send a request with status code checking. Parameters ---------- method : `str` Method for the request. url : `str` URL for the request. **kwargs Keyword arguments passed to :meth:`requests.request`. Returns ------- resp : `requests.Response` Response to the request. """ if self._expiry is not None and datetime.datetime.now() > self._expiry: self._refresh_access_token() r = self.session.request(method, url, **kwargs) if r.status_code not in range(200, 299): try: error = r.json() if "errors" in error: error = error["errors"][0] emsg = ( f"{r.status_code} {error['code']}: {error['detail']}" ) else: emsg = f"{r.status_code} {r.reason}: {error['detail']}" except requests.exceptions.JSONDecodeError: emsg = f"{r.status_code} {r.reason}" raise RuntimeError(emsg) return r
[docs] def set_access_token( self, access_token: str = None, *, refresh_token: str = None, expiry: Union[str, datetime.datetime] = None, ) -> None: """ Set the TIDAL API access token. Parameters ---------- access_token : `str`, optional Access token. If not provided, an access token is obtained using an OAuth 2.0 authorization flow. refresh_token : `str`, keyword-only, optional Refresh token accompanying `access_token`. expiry : `str` or `datetime.datetime`, keyword-only, optional Access token expiry timestamp in the ISO 8601 format :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be reauthenticated using the refresh token (if available) or the default authorization flow (if possible) when `access_token` expires. """ if access_token is None: if not self._client_id or not self._client_secret: raise ValueError("TIDAL API client credentials not provided.") if self._flow == "client_credentials": client_b64 = base64.urlsafe_b64encode( f"{self._client_id}:{self._client_secret}".encode() ).decode() r = requests.post( self.TOKEN_URL, data={"grant_type": "client_credentials"}, headers={"Authorization": f"Basic {client_b64}"}, ).json() else: client_b64 = base64.urlsafe_b64encode( f"{self._client_id}:{self._client_secret}".encode() ).decode() data = { "grant_type": "authorization_code", "redirect_uri": self._redirect_uri, } if self._flow == "pkce": data["client_id"] = self._client_id data["code_verifier"] = secrets.token_urlsafe(96) data["code"] = self._get_authorization_code( base64.urlsafe_b64encode( hashlib.sha256( data["code_verifier"].encode() ).digest() ) .decode() .replace("=", "") ) else: data["code"] = self._get_authorization_code() r = requests.post( self.TOKEN_URL, data=data, headers={"Authorization": f"Basic {client_b64}"}, ).json() refresh_token = r["refresh_token"] access_token = r["access_token"] expiry = datetime.datetime.now() + datetime.timedelta( 0, r["expires_in"] ) if self._save: _config[self._NAME] = { "flow": self._flow, "client_id": self._client_id, "access_token": access_token, "expiry": expiry.strftime("%Y-%m-%dT%H:%M:%SZ"), "scopes": self._scopes, } if refresh_token: _config[self._NAME]["refresh_token"] = refresh_token for attr in ("client_secret", "redirect_uri"): if hasattr(self, f"_{attr}"): _config[self._NAME][attr] = ( getattr(self, f"_{attr}") or "" ) with open(DIR_HOME / "minim.cfg", "w") as f: _config.write(f) self.session.headers["Authorization"] = f"Bearer {access_token}" self._refresh_token = refresh_token self._expiry = ( datetime.datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ") if isinstance(expiry, str) else expiry ) if self._flow == "pkce": self._user_id = self.get_me()["data"]["id"]
[docs] def set_flow( self, flow: str, *, client_id: str = None, client_secret: str = None, browser: bool = False, web_framework: str = None, port: Union[int, str] = 8888, redirect_uri: str = None, scopes: Union[str, list[str]] = "", save: bool = True, ) -> None: """ Set the authorization flow. Parameters ---------- flow : `str` Authorization flow. .. container:: **Valid values**: * :code:`"client_credentials"` for the client credentials flow. client_id : `str`, keyword-only, optional Client ID. Required for all authorization flows. client_secret : `str`, keyword-only, optional Client secret. Required for all authorization flows. browser : `bool`, keyword-only, default: :code:`False` Determines whether a web browser is automatically opened for the authorization code (with PKCE) flow. If :code:`False`, users will have to manually open the authorization URL. Not applicable when `web_framework="playwright"`. web_framework : `str`, keyword-only, optional Web framework used to automatically complete the authorization code (with PKCE) flow. .. container:: **Valid values**: * :code:`"http.server"` for the built-in implementation of HTTP servers. * :code:`"flask"` for the Flask framework. * :code:`"playwright"` for the Playwright framework. port : `int` or `str`, keyword-only, default: :code:`8888` Port on :code:`localhost` to use for the authorization code flow with the :code:`http.server` and Flask frameworks. redirect_uri : `str`, keyword-only, optional Redirect URI for the authorization code flow. If not specified, an open port on :code:`localhost` will be used. scopes : `str` or `list`, keyword-only, optional Authorization scopes to request access to in the authorization code flow. save : `bool`, keyword-only, default: :code:`True` Determines whether to save the newly obtained access tokens and their associated properties to the Minim configuration file. """ if flow not in self._FLOWS: emsg = ( f"Invalid authorization flow ({flow=}). " f"Valid values: {', '.join(self._FLOWS)}." ) raise ValueError(emsg) self._flow = flow self._save = save self._client_id = client_id or os.environ.get("TIDAL_CLIENT_ID") self._client_secret = client_secret or os.environ.get( "TIDAL_CLIENT_SECRET" ) if flow == "pkce": self._browser = browser self._scopes = ( " ".join(scopes) if isinstance(scopes, list) else scopes ) if redirect_uri: self._redirect_uri = redirect_uri if "localhost" in redirect_uri: self._port = re.search( r"localhost:(\d+)", redirect_uri ).group(1) elif web_framework: wmsg = ( "The redirect URI is not on localhost, " "so automatic authorization code " "retrieval is not available." ) logging.warning(wmsg) web_framework = None elif port: self._port = port self._redirect_uri = f"http://localhost:{port}/callback" else: self._port = self._redirect_uri = None self._web_framework = ( web_framework if web_framework in {None, "http.server"} or globals()[f"FOUND_{web_framework.upper()}"] else None ) if self._web_framework is None and web_framework: wmsg = ( f"The {web_framework.capitalize()} web " "framework was not found, so automatic " "authorization code retrieval is not " "available." ) warnings.warn(wmsg) elif flow == "client_credentials": self._scopes = ""
### ALBUMS ################################################################
[docs] def get_album( self, album_id: Union[int, str], /, country_code: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Albums > Get single album <https://tidal-music.github.io /tidal-api-reference/#/albums/get_albums__id_>`_: Retrieve a single album by ID. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Example**: :code:`251380836`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"artists"`, :code:`"coverArt"`, :code:`"genres"`, :code:`"items"`, :code:`"owners"`, :code:`"providers"`, and :code:`"similarAlbums"`. **Examples**: :code:`"artists"`, :code:`"artists,coverArt"`, and :code:`["artists", "coverArt"]`. Returns ------- album : `dict` TIDAL catalog information and related resources for a single album. """ if isinstance(include, str) and "," in include: include = include.split(",") return self._get_json( f"{self.API_URL}/albums/{album_id}", params={"countryCode": country_code, "include": include}, )
[docs] def get_albums( self, country_code: str, *, album_ids: Union[int, str, list[Union[int, str]], None] = None, barcode_ids: Union[int, str, list[Union[int, str]], None] = None, user_ids: Union[int, str, list[Union[int, str]], None] = None, include: Union[str, list[str], None] = None, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get multiple albums <https://tidal-music.github.io /tidal-api-reference/#/albums/get_albums>`_: Retrieve multiple albums using available filters. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. album_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL album ID(s). Only optional if either `barcode_ids` or `user_ids` is provided. **Examples**: :code:`251380836`, :code:`"251380836"`, :code:`"251380836,275646830"`, :code:`[251380836, 275646830]`, and :code:`["251380836", "275646830"]`. barcode_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL album barcode ID(s). Only optional if either `album_ids` or `user_ids` is provided. **Examples**: :code:`196589525444`, :code:`"196589525444"`, :code:`"196589525444,075679933652"`, :code:`[196589525444, 075679933652]`, and :code:`["196589525444", "075679933652"]` user_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL user ID(s). Only optional if either `album_ids` or `barcode_ids` is provided. **Examples**: :code:`123456`, :code:`"123456"`, :code:`"123456,789012"`, :code:`[123456, 789012]`, and :code:`["123456, 789012"]`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"artists"`, :code:`"coverArt"`, :code:`"genres"`, :code:`"items"`, :code:`"owners"`, :code:`"providers"`, and :code:`"similarAlbums"`. **Examples**: :code:`"artists"`, :code:`"artists,coverArt"`, and :code:`["artists", "coverArt"]`. cursor : `int` or `str`, keyword-only, optional Pagination cursor. If not specified, the first page of results will be returned. Returns ------- albums : `dict` A dictionary containing TIDAL catalog information and related resources for multiple albums. """ if isinstance(include, str) and "," in include: include = include.split(",") if isinstance(user_ids, str) and "," in user_ids: user_ids = user_ids.split(",") if isinstance(album_ids, str) and "," in album_ids: album_ids = album_ids.split(",") if isinstance(barcode_ids, str) and "," in barcode_ids: barcode_ids = barcode_ids.split(",") return self._get_json( f"{self.API_URL}/albums", params={ "countryCode": country_code, "page[cursor]": cursor, "include": include, "filter[r.owners.id]": user_ids, "filter[id]": album_ids, "filter[barcodeId]": barcode_ids, }, )
[docs] def get_album_relationship( self, album_id: Union[int, str], relationship: str, /, *, include: bool = False, cursor: Union[int, str, None] = None, **kwargs, ) -> dict[str, Any]: """ Retrieve information related to an album. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. relationship : `str`, positional-only Relationship type. **Valid values**: :code:`"artists"`, :code:`"coverArt"`, :code:`"items"`, :code:`"owners"`, :code:`"providers"`, or :code:`"similarAlbums"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. **kwargs Keyword arguments. Returns ------- album_relationship : `dict` A dictionary containing TIDAL catalog information for the specified album relationship. """ return self._get_json( f"{self.API_URL}/albums/{album_id}/relationships/{relationship}", params={ "page[cursor]": cursor, "include": relationship if include else None, **kwargs, }, )
[docs] def get_album_artists( self, album_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get album artists <https://tidal-music.github.io /tidal-api-reference/#/albums /get_albums__id__relationships_artists>`_: Retrieve main artists associated with an album. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- artists : `dict` A dictionary containing TIDAL catalog information for the main artists associated with the album. """ return self.get_album_relationship( album_id, "artists", countryCode=country_code, include=include, cursor=cursor, )
[docs] def get_album_cover_art( self, album_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get album cover art <https://tidal-music.github.io /tidal-api-reference/#/albums /get_albums__id__relationships_coverArt>`_: Retrieve cover artwork associated with an album. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- cover_art : `dict` A dictionary containing TIDAL catalog information for the cover artwork associated with the album. """ return self.get_album_relationship( album_id, "coverArt", countryCode=country_code, include=include, cursor=cursor, )
[docs] def get_album_items( self, album_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get album items <https://tidal-music.github.io /tidal-api-reference/#/albums /get_albums__id__relationships_items>`_: Retrieve items in an album. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- album_items : `dict` A dictionary containing TIDAL catalog information for the items in the album. """ return self.get_album_relationship( album_id, "items", countryCode=country_code, include=include, cursor=cursor, )
[docs] def get_album_owners( self, album_id: Union[int, str], /, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get album owners <https://tidal-music.github.io /tidal-api-reference/#/albums /get_albums__id__relationships_owners>`_: Retrieve TIDAL catalog entries that contain an album. .. admonition:: User authentication :class: warning Requires user authentication via the authorization code flow. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- owners : `dict` A dictionary containing TIDAL catalog information for the owners of the album. """ self._check_authentication("get_album_owners") return self.get_album_relationship( album_id, "owners", include=include, cursor=cursor )
[docs] def get_album_providers( self, album_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get album providers <https://tidal-music.github.io /tidal-api-reference/#/albums /get_albums__id__relationships_providers>`_: Retrieve providers of an album. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- providers : `dict` A dictionary containing TIDAL catalog information for the providers of the album. """ return self.get_album_relationship( album_id, "providers", countryCode=country_code, include=include, cursor=cursor, )
[docs] def get_similar_albums( self, album_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Albums > Get similar albums <https://tidal-music.github.io /tidal-api-reference/#/albums /get_albums__id__relationships_similarAlbums>`_: Retrieve albums similar to a given album. Parameters ---------- album_id : `int` or `str`, positional-only TIDAL album ID. **Examples**: :code:`251380836` and :code:`"251380836"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- similar_albums : `dict` A dictionary containing TIDAL catalog information for the similar albums. """ return self.get_album_relationship( album_id, "similarAlbums", countryCode=country_code, include=include, cursor=cursor, )
### ARTIST ROLES ##########################################################
[docs] def get_role(self, artist_role_id: Union[int, str], /) -> dict[str, Any]: """ `Artist Roles > Get single artist role <https://tidal-music.github.io/tidal-api-reference/# /artist-roles/get_artist_roles__id_>`_: Retrieves a single artist role. Parameters ---------- artist_role_id : `int` or `str` TIDAL artist role ID. **Examples**: :code:`1` and :code:`"1"`. Returns ------- artist_role : `dict` A dictionary containing TIDAL catalog information for the artist role. """ return self.session.get(f"{self.API_URL}/artistRoles/{artist_role_id}")
[docs] def get_roles( self, artist_role_ids: Union[int, str, list[Union[int, str]]], ) -> dict[str, Any]: """ `Artist Roles > Get multiple artist roles <https://tidal-music.github.io/tidal-api-reference/# /artist-roles/get_artist_roles>`_: Retrieves multiple artist roles. Parameters ---------- artist_role_ids : `int`, `str`, or `list` TIDAL artist role ID(s). **Examples**: :code:`1`, :code:`"1"`, :code:`"1,2"`, :code:`[1, 2]`, and :code:`["1", "2"]`. Returns ------- artist_roles : `dict` A dictionary containing TIDAL catalog information for the artists' roles. """ if isinstance(artist_role_ids, str) and "," in artist_role_ids: artist_role_ids = artist_role_ids.split(",") return self.session.get( f"{self.API_URL}/artistRoles", params={"filter[id]": artist_role_ids}, )
### ARTISTS ###############################################################
[docs] def get_artist( self, artist_id: str, /, country_code: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Artists > Get single artist <https://tidal-music.github.io /tidal-api-reference/#/artists/get_artists__id_>`_: Retrieves a single artist. Parameters ---------- artist_id : `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"biography"`, :code:`"followers"`, :code:`"following"`, :code:`"owners"`, :code:`"profileArt"`, :code:`"radio"`, :code:`"roles"`, :code:`"similarArtists"`, :code:`"trackProviders"`, :code:`"tracks"`, :code:`"videos"`. **Examples**: :code:`"albums"`, :code:`"albums,biography"`, and :code:`["albums", "biography"]`. Returns ------- artist : `dict` A dictionary containing TIDAL catalog information for the artist. """ if isinstance(include, str) and "," in include: include = include.split(",") return self.session.get( f"{self.API_URL}/artists/{artist_id}", params={"country": country_code, "include": include}, )
[docs] def get_artists( self, country_code: str, *, include: Union[str, list[str], None] = None, artist_ids: Union[int, str, list[Union[int, str]], None] = None, artist_handles: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Artists > Get multiple artists <https://tidal-music.github.io /tidal-api-reference/#/artists/get_artists>`_: Retrieves multiple artists using available filters. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"biography"`, :code:`"followers"`, :code:`"following"`, :code:`"owners"`, :code:`"profileArt"`, :code:`"radio"`, :code:`"roles"`, :code:`"similarArtists"`, :code:`"trackProviders"`, :code:`"tracks"`, :code:`"videos"`. **Examples**: :code:`"albums"`, :code:`"albums,biography"`, and :code:`["albums", "biography"]`. artist_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL artist ID(s). Only optional if `artist_handles` is provided. **Examples**: :code:`1`, :code:`"1"`, :code:`"1,2"`, :code:`[1, 2]`, and :code:`["1", "2"]`. artist_handles : `str` or `list`, keyword-only, optional TIDAL artist handle(s). Only optional if `artist_ids` is provided. **Examples**: :code:`"artist_handle"` and :code:`["artist_handle"]`. Returns ------- artists : `dict` A dictionary containing TIDAL catalog information for the artists. """ if isinstance(include, str) and "," in include: include = include.split(",") if isinstance(artist_ids, str) and "," in artist_ids: artist_ids = artist_ids.split(",") if isinstance(artist_handles, str) and "," in artist_handles: artist_handles = artist_handles.split(",") return self.session.get( f"{self.API_URL}/artists", params={ "country": country_code, "include": include, "filter[handle]": artist_handles, "filter[id]": artist_ids, }, )
[docs] def get_artist_relationship( self, artist_id: Union[int, str], relationship: str, /, include: bool = False, **kwargs, ) -> dict[str, Any]: """ Retrieve information related to an artist. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. relationship : `str`, positional-only Relationship type. **Valid values**: :code:`"albums"`, :code:`"biography"`, :code:`"owners"`, :code:`"profileArt"`, :code:`"radio"`, :code:`"roles"`, :code:`"similarArtists"`, :code:`"trackProviders"`, :code:`"tracks"`, or :code:`"videos"`. include : bool, keyword-only Specifies whether to include TIDAL content metadata in the response. **kwargs Keyword arguments. Returns ------- album_relationship : `dict` A dictionary containing TIDAL catalog information for the specified album relationship. """ return self._get_json( f"{self.API_URL}/artists/{artist_id}/relationships/{relationship}", params={"include": relationship if include else None, **kwargs}, )
[docs] def get_artist_albums( self, artist_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's albums <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_albums>`_: Retrieve an artist's albums. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- albums : `dict` A dictionary containing TIDAL catalog information for the artist's albums. """ return self.get_artist_relationship( artist_id, "albums", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_artist_biography( self, artist_id: Union[int, str], /, country_code: str, *, include: bool = False, ) -> dict[str, Any]: """ `Artists > Get artist's biography <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_biography>`_: Retrieve an artist's biography. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. Returns ------- biography : `dict` A dictionary containing TIDAL catalog information for the artist's biography. """ return self.get_artist_relationship( artist_id, "biography", countryCode=country_code, include=include )
[docs] def get_artist_owners( self, artist_id: Union[int, str], /, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's owners <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_owners>`_: Retrieve TIDAL catalog entries that contain an artist. .. admonition:: User authentication :class: warning Requires user authentication via the authorization code flow. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- owners : `dict` A dictionary containing TIDAL catalog information for the owners of the artist. """ self._check_authentication("get_artist_owners") return self.get_artist_relationship( artist_id, "owners", include=include, **{"page[cursor]": cursor} )
[docs] def get_artist_profile_art( self, artist_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's profile artwork <https://tidal-music.github.io/tidal-api-reference/#/artists /get_artists__id__relationships_profileArt>`_: Retrieve an artist's profile artwork. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- profile_art : `dict` A dictionary containing TIDAL catalog information for the artist's profile artwork. """ return self.get_artist_relationship( artist_id, "profile_art", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_artist_radio( self, artist_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's radio <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_radio>`_: Retrieve an artist's radio. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- radio : `dict` A dictionary containing TIDAL catalog information for the artist's radio. """ return self.get_artist_relationship( artist_id, "radio", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_artist_roles( self, artist_id: Union[int, str], /, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's roles <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_roles>`_: Retrieve an artist's roles. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- roles : `dict` A dictionary containing TIDAL catalog information for the artist's roles. """ return self.get_artist_relationship( artist_id, "roles", include=include, **{"page[cursor]": cursor}, )
[docs] def get_similar_artists( self, artist_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get similar artists <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_similarArtists>`_: Retrieve similar artists for a given artist. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- similar_artists : `dict` A dictionary containing TIDAL catalog information for similar artists. """ return self.get_artist_relationship( artist_id, "similar", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_artist_track_providers( self, artist_id: Union[int, str], /, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's track providers <https://tidal-music.github.io/tidal-api-reference/#/artists /get_artists__id__relationships_trackProviders>`_: Retrieve an artist's track providers. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- track_providers : `dict` A dictionary containing TIDAL catalog information for the artist's track providers. """ return self.get_artist_relationship( artist_id, "trackProviders", include=include, **{"page[cursor]": cursor}, )
[docs] def get_artist_tracks( self, artist_id: Union[int, str], /, country_code: str, *, collapse_by: str = "ID", include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's tracks <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_tracks>`_: Retrieve an artist's tracks. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. collapse_by : `str`, keyword-only, default: :code:`"ID"` Collapse-by option. **Valid values**: :code:`"FINGERPRINT"` to collapse similar tracks based on entry fingerprints, or :code:`"ID"` to always return all available items. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- tracks : `dict` A dictionary containing TIDAL catalog information for the artist's tracks. """ return self.get_artist_relationship( artist_id, "tracks", countryCode=country_code, collapseBy=collapse_by, include=include, **{"page[cursor]": cursor}, )
[docs] def get_artist_videos( self, artist_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Artists > Get artist's videos <https://tidal-music.github.io /tidal-api-reference/#/artists /get_artists__id__relationships_videos>`_: Retrieve an artist's videos. Parameters ---------- artist_id : `int` or `str`, positional-only TIDAL artist ID. **Examples**: :code:`1566` and :code:`"1566"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- videos : `dict` A dictionary containing TIDAL catalog information for the artist's videos. """ return self.get_artist_relationship( artist_id, "videos", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
### ARTWORKS ##############################################################
[docs] def get_artwork( self, artwork_id: str, /, country_code: str, *, include: Union[str, list[str]] = None, ) -> dict[str, Any]: """ `Artworks > Get single artwork <https://tidal-music.github.io /tidal-api-reference/#/artworks/get_artworks__id_>`_: Retrieve a single artwork. Parameters ---------- artwork_id : `str`, positional-only TIDAL artwork ID. **Example**: :code:`"a468bee88def"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid value**: :code:`"owners"`. Returns ------- artwork : `dict` A dictionary containing TIDAL catalog information for the artwork. """ if isinstance(include, str) and "," in include: include = include.split(",") return self._get_json( f"{self.API_URL}/artworks/{artwork_id}", params={"countryCode": country_code, "include": include}, )
[docs] def get_artworks( self, artwork_ids: Union[str, list[str]], /, country_code: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Artworks > Get multiple artworks <https://tidal-music.github.io /tidal-api-reference/#/artworks/get_artworks>`_: Retrieve multiple artworks. Parameters ---------- artwork_ids : `str` or `list`, positional-only TIDAL artwork ID(s). **Example**: :code:`"a468bee88def"`, :code:`"a468bee88def,9344c45a869c"`, or :code:`["a468bee88def", "9344c45a869c"]`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid value**: :code:`"owners"`. Returns ------- artworks : `dict` A dictionary containing TIDAL catalog information for the artworks. """ if isinstance(include, str) and "," in include: include = include.split(",") return self._get_json( f"{self.API_URL}/artworks", params={ "countryCode": country_code, "include": include, "filter[id]": artwork_ids, }, )
[docs] def get_artwork_owners( self, artwork_id: str, /, *, include: bool = False, cursor: Union[int, str, None], ) -> dict[str, Any]: """ `Artworks > Get artwork owners <https://tidal-music.github.io /tidal-api-reference/#/artworks /get_artworks__id__relationships_owners>`_: Retrieve the owners of an artwork. .. admonition:: User authentication :class: warning Requires user authentication via the authorization code flow. Parameters ---------- artwork_id : `str`, positional-only TIDAL artwork ID. **Example**: :code:`"a468bee88def"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- owners : `dict` A dictionary containing TIDAL catalog information for the artwork owners. """ self._check_authentication("get_artwork_owners") return self._get_json( f"{self.API_URL}/artworks/{artwork_id}/relationships/owners", params={ "include": "owners" if include else None, "page[cursor]": cursor, }, )
### PLAYLISTS #############################################################
[docs] def get_playlist( self, playlist_uuid: str, /, country_code: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Playlists > Get single playlist <https://tidal-music.github.io /tidal-api-reference/#/playlists/get_playlists__id_>`_: Retrieve a single playlist. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"coverArt"`, :code:`"items"`, or :code:`"owners"`. Returns ------- playlist : `dict` A dictionary containing TIDAL catalog information for the playlist. """ if isinstance(include, str) and "," in include: include = include.split(",") return self._get_json( f"{self.API_URL}/playlists/{playlist_uuid}", params={ "countryCode": country_code, "include": include, }, )
[docs] def get_playlists( self, country_code: str, *, playlist_uuids: Union[str, list[str], None] = None, user_ids: Union[int, str, list[Union[int, str]], None] = None, include: Union[str, list[str], None] = None, cursor: Union[int, str, None] = None, sort: str = None, ) -> dict[str, Any]: """ `Playlists > Get multiple playlists <https://tidal-music.github.io /tidal-api-reference/#/playlists/get_playlists>`_: Retrieve multiple playlists. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.read` scope. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. playlist_uuids : `str` or `list`, keyword-only, optional TIDAL playlist UUID(s). Only optional if `user_ids` is provided. **Examples**: :code:`"550e8400-e29b-41d4-a716-446655440000"` and :code:`["550e8400-e29b-41d4-a716-446655440000", "4261748a-4287-4758-aaab-6d5be3e99e52"]`. user_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL user ID(s). Only optional if `playlist_uuids` is provided. **Examples**: :code:`123456`, :code:`"123456"`, :code:`"123456,789012"`, :code:`[123456, 789012]`, and :code:`["123456, 789012"]`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"coverArt"`, :code:`"items"`, or :code:`"owners"`. cursor : `int` or `str`, keyword-only, optional Pagination cursor. If not specified, the first page of results will be returned. sort : str, keyword-only, optional Field to sort the returned playlists by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. **Valid values**: :code:`"createdAt"`, :code:`"-createdAt`, :code:`"lastModifiedAt`, :code:`"-lastModifiedAt"`, :code:`"name"`, :code:`"-name"`. Returns ------- playlists : `dict` A dictionary containing TIDAL catalog information for the playlists. """ self._check_scope("get_playlists", "playlists.read") if isinstance(include, str) and "," in include: include = include.split(",") if isinstance(user_ids, str) and "," in user_ids: user_ids = user_ids.split(",") if isinstance(playlist_uuids, str) and "," in playlist_uuids: playlist_uuids = playlist_uuids.split(",") return self._get_json( f"{self.API_URL}/playlists", params={ "countryCode": country_code, "page[cursor]": cursor, "include": include, "filter[r.owners.id]": user_ids, "filter[id]": playlist_uuids, "sort": sort, }, )
[docs] def create_playlist( self, name: str, /, *, description: Union[str, None] = None, public: bool = True, ) -> dict[str, Any]: """ `Playlists > Create single playlist <https://tidal-music.github.io/tidal-api-reference/#/playlists /post_playlist>`_: Create a new playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.write` scope. Parameters ---------- name : `str`, positional-only Playlist name. description : `str`, keyword-only, optional Playlist description. public : `bool`, keyword-only, default: :code:`True` Specifies whether the playlist is public. Returns ------- playlist : `dict` A dictionary containing the created playlist's information. """ self._check_scope("create_playlist", "playlists.write") attributes = { "name": name, "accessType": "PUBLIC" if public else "UNLISTED", } if description is not None: attributes["description"] = description return self._request( "post", f"{self.API_URL}/playlists", json={"data": {"attributes": attributes, "type": "playlists"}}, )
[docs] def update_playlist( self, playlist_uuid: str, /, *, name: Union[str, None] = None, description: Union[str, None] = None, public: Union[bool, None] = None, ) -> None: """ `Playlists > Update single playlist <https://tidal-music.github.io/tidal-api-reference/#/playlists /patch_playlists__id_>`_: Update an existing playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.write` scope. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. name : `str`, keyword-only, optional New name for the playlist. description : `str`, keyword-only, optional New description for the playlist. public : `bool`, keyword-only, optional Specifies whether the playlist is public. """ self._check_scope("create_playlist", "playlists.write") attributes = {} if name is not None: attributes["name"] = name if description is not None: attributes["description"] = description if public is not None: attributes["accessType"] = "PUBLIC" if public else "UNLISTED" self._request( "patch", f"{self.API_URL}/playlists/{playlist_uuid}", json={ "data": { "attributes": attributes, "id": playlist_uuid, "type": "playlists", } }, )
[docs] def delete_playlist(self, playlist_uuid: str, /) -> None: """ `Playlists > Delete single playlist <https://tidal-music.github.io/tidal-api-reference/#/playlists /delete_playlists__id_>`_: Delete an existing playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.write` scope. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. """ self._check_scope("create_playlist", "playlists.write") self._request("delete", f"{self.API_URL}/playlists/{playlist_uuid}")
[docs] def get_playlist_relationship( self, playlist_uuid: str, relationship: str, /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ Retrieve information related to a playlist. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. relationship : `str`, positional-only Relationship type. **Valid values**: :code:`"coverArt"`, :code:`"items"`, or :code:`"owners"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- album_relationship : `dict` A dictionary containing TIDAL catalog information for the specified album relationship. """ return self._get_json( f"{self.API_URL}/playlists/{playlist_uuid}/relationships/{relationship}", params={ "countryCode": country_code, "include": relationship if include else None, "page[cursor]": cursor, }, )
[docs] def get_playlist_cover_art( self, playlist_uuid: str, /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Playlists > Get playlist cover art <https://tidal-music.github.io/tidal-api-reference/#/playlists /get_playlists__id__relationships_coverArt>`_: Retrieve the cover art for a playlist. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. If not specified, the first page of results will be returned. Returns ------- cover_art : `dict` A dictionary containing TIDAL catalog information for the cover artwork associated with the playlist. """ return self.get_playlist_relationship( playlist_uuid, "coverArt", country_code, include=include, cursor=cursor, )
[docs] def get_playlist_items( self, playlist_uuid: str, /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Playlists > Get playlist items <https://tidal-music.github.io/tidal-api-reference/#/playlists /get_playlists__id__relationships_items>`_: Retrieve the items in a playlist. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. If not specified, the first page of results will be returned. Returns ------- owners : `dict` A dictionary containing TIDAL catalog information for the items in the playlist. """ return self.get_playlist_relationship( playlist_uuid, "items", country_code, include=include, cursor=cursor, )
[docs] def add_playlist_items( self, playlist_uuid: str, items: list[tuple[Union[int, str], str], dict[str, str]], before_item_uuid: Union[int, str] = None, ) -> None: """ `Playlists > Add playlist items <https://tidal-music.github.io/tidal-api-reference/#/playlists /post_playlists__id__relationships_items>`_: Add items to a playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.write` scope. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. items : `list` A list of items to add to the playlist. Each item can be either a tuple of :code:`(id, type)` or a dictionary with keys :code:`id` and :code:`type`, where :code:`id` is the TIDAL item ID and :code:`type` is the item type (:code:`"tracks"` or :code:`"videos"`). before_item_uuid : `int` or `str`, optional UUID of the item before which the new items should be added. If not specified, the items will be appended to the end of the playlist. """ self._check_scope("add_playlist_items", "playlists.write") for idx, item in enumerate(items): if isinstance(item, tuple): if len(item) != 2: raise ValueError( "Tuples in `items` passed to " f"`{self._NAME}.add_playlist_items()` must " "have length 2." ) if not isinstance(item[0], str) or not item[1].endswith("s"): items[idx] = item = str(item[0]), f"{item[1]}s" items[idx] = {"id": item[0], "type": item[1]} else: if "id" not in item or "type" not in item: raise ValueError( "Dictionaries in `items` passed to " f"`{self._NAME}.add_playlist_items()` must have " "keys 'id' and 'type'." ) if not isinstance(item["id"], str): item["id"] = str(item["id"]) if not item["type"].endswith("s"): item["type"] = f"{item['type']}s" body = {"data": items} if before_item_uuid is not None: body["meta"] = {"positionBefore": str(before_item_uuid)} self._request( "post", f"{self.API_URL}/playlists/{playlist_uuid}/relationships/items", json=body, )
[docs] def update_playlist_items( self, playlist_uuid: str, items: list[tuple[Union[int, str], str, str], dict[str, str]], before_item_uuid: Union[int, str], ) -> None: """ `Playlists > Update playlist items <https://tidal-music.github.io/tidal-api-reference/#/playlists /patch_playlists__id__relationships_items>`_: Update items in a playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.write` scope. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. items : `list` A list of items to update in the playlist. Each item can be either a tuple of :code:`(id, type, uuid)` or a dictionary with keys :code:`id`, :code:`type`, and :code:`meta`, where :code:`id` is the TIDAL item ID, :code:`type` is the item type (:code:`"tracks"` or :code:`"videos"`), and :code:`uuid` is the TIDAL item UUID. before_item_uuid : `int` or `str` UUID of the item before which the items should be moved to. """ self._check_scope("update_playlist_items", "playlists.write") for idx, item in enumerate(items): if isinstance(item, tuple): if len(item) != 3: raise ValueError( "Tuples in `items` passed to " f"`{self._NAME}.update_playlist_items()` must " "have length 3." ) if not isinstance(item[0], str) or not item[1].endswith("s"): items[idx] = item = str(item[0]), f"{item[1]}s", item[2] item = { "id": item[0], "type": item[1], "meta": {"itemId": item[2]}, } else: if any(key not in item for key in {"id", "type", "meta"}): raise ValueError( "Dictionaries in `items` passed to " f"`{self._NAME}.update_playlist_items()` must " "have keys 'id', 'type', and 'meta'." ) if "itemId" not in item["meta"]: raise ValueError( "Track UUID not provided in `meta/itemId` for " f"{item['type'][:-1]} ID '{item['id']}'." ) if not isinstance(item["id"], str): item["id"] = str(item["id"]) if not item["type"].endswith("s"): item["type"] = f"{item['type']}s" self._request( "patch", f"{self.API_URL}/playlists/{playlist_uuid}/relationships/items", json={"data": items, "positionBefore": str(before_item_uuid)}, )
[docs] def delete_playlist_items( self, playlist_uuid: str, items: list[tuple[Union[int, str], str, str], dict[str, str]], ) -> None: """ `Playlists > Delete playlist items <https://tidal-music.github.io/tidal-api-reference/#/playlists /delete_playlists__id__relationships_items>`_: Remove items from a playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.write` scope. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. items : `list` A list of items to remove from the playlist. Each item can be either a tuple of :code:`(id, type, uuid)` or a dictionary with keys :code:`id`, :code:`type`, and :code:`meta`, where :code:`id` is the TIDAL item ID, :code:`type` is the item type (:code:`"tracks"` or :code:`"videos"`), and :code:`uuid` is the TIDAL item UUID. """ self._check_scope("delete_playlist_items", "playlists.write") for idx, item in enumerate(items): if isinstance(item, tuple): if len(item) != 3: raise ValueError( "Tuples in `items` passed to " f"`{self._NAME}.delete_playlist_items()` must " "have length 3." ) if not isinstance(item[0], str) or not item[1].endswith("s"): items[idx] = item = str(item[0]), f"{item[1]}s", item[2] item = { "id": item[0], "type": item[1], "meta": {"itemId": item[2]}, } else: if any(key not in item for key in {"id", "type", "meta"}): raise ValueError( "Dictionaries in `items` passed to " f"`{self._NAME}.delete_playlist_items()` must " "have keys 'id', 'type', and 'meta'." ) if "itemId" not in item["meta"]: raise ValueError( "Track UUID not provided in `meta/itemId` for " f"{item['type'][:-1]} ID '{item['id']}'." ) if not isinstance(item["id"], str): item["id"] = str(item["id"]) if not item["type"].endswith("s"): item["type"] = f"{item['type']}s" self._request( "delete", f"{self.API_URL}/playlists/{playlist_uuid}/relationships/items", json={"data": items}, )
[docs] def get_playlist_owners( self, playlist_uuid: str, /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Playlists > Get playlist owners <https://tidal-music.github.io/tidal-api-reference/#/playlists /get_playlists__id__relationships_owners>`_: Retrieve the owners of a playlist. .. admonition:: Authorization scope :class: warning Requires the :code:`playlists.read` scope. Parameters ---------- playlist_uuid : `str`, positional-only TIDAL playlist UUID. **Example**: :code:`"550e8400-e29b-41d4-a716-446655440000"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. If not specified, the first page of results will be returned. Returns ------- owners : `dict` A dictionary containing TIDAL catalog information for the owners of the playlist. """ self._check_scope("get_playlist_owners", "playlists.read") return self.get_playlist_relationship( playlist_uuid, "owners", country_code, include=include, cursor=cursor, )
### PROVIDERS #############################################################
[docs] def get_provider(self, provider_id: Union[int, str], /) -> dict[str, Any]: """ `Providers > Get provider <https://tidal-music.github.io /tidal-api-reference/#/providers/get_providers__id_>`_: Retrieve a specific provider by ID. Parameters ---------- provider_id : `int` or `str`, positional-only TIDAL provider ID. Returns ------- provider : `dict` A dictionary containing TIDAL catalog information for the provider. """ return self._get_json(f"{self.API_URL}/providers/{provider_id}")
[docs] def get_providers( self, provider_ids: Union[int, str, list[Union[int, str]]], / ) -> dict[str, Any]: """ `Providers > Get providers <https://tidal-music.github.io /tidal-api-reference/#/providers/get_providers>`_: Retrieve multiple providers by their IDs. Parameters ---------- provider_ids : `int`, `str`, or `list`, positional-only TIDAL provider ID(s). Returns ------- providers : `dict` A dictionary containing TIDAL catalog information for the providers. """ return self._get_json( f"{self.API_URL}/providers", params={"filter[id]": provider_ids} )
### SEARCH RESULTS ########################################################
[docs] def search( self, query: str, /, country_code: str, *, explicit: bool = True, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Search Results > Search <https://tidal-music.github.io/tidal-api-reference/# /searchResults/get_searchResults__id_>`_: Search for content in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"playlists"`, :code:`"topHits"`, :code:`"tracks"`, or :code:`"videos"`. **Examples**: :code:`"albums"`, :code:`"albums,artists"`, and :code:`["albums", "artists"]`. Returns ------- results : `dict` A dictionary containing the search results. """ self._check_scope("search", "search.read") params = { "countryCode": country_code, "explicitFilter": "include" if explicit else "exclude", "include": include, } return self._get_json( f"{self.API_URL}/searchResults/{urllib.parse.quote(query)}", params=params, )
[docs] def search_relationship( self, query: str, relationship: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ Retrieve information based on a search query. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- query : `str`, positional-only Search query. relationship : `str`, positional-only Relationship type. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"playlists"`, :code:`"topHits"`, :code:`"tracks"`, or :code:`"videos"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- results : `dict` A dictionary containing the search results. """ self._check_scope("search", "search.read") return self._get_json( f"{self.API_URL}/searchResults/{urllib.parse.quote(query)}" f"/relationships/{relationship}", params={ "countryCode": country_code, "explicitFilter": "include" if explicit else "exclude", "include": relationship if include else None, "page[cursor]": cursor, }, )
[docs] def search_albums( self, query: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Results > Search albums <https://tidal-music.github.io /tidal-api-reference/#/searchResults /get_searchResults__id__relationships_albums>`_: Search for albums in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- albums : `dict` A dictionary containing matching albums. """ return self.search_relationship( query, "albums", country_code, explicit=explicit, include=include, cursor=cursor, )
[docs] def search_artists( self, query: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Results > Search artists <https://tidal-music.github.io /tidal-api-reference/#/searchResults /get_searchResults__id__relationships_artists>`_: Search for artists in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- artists : `dict` A dictionary containing matching artists. """ return self.search_relationship( query, "artists", country_code, explicit=explicit, include=include, cursor=cursor, )
[docs] def search_playlists( self, query: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Results > Search playlists <https://tidal-music.github.io /tidal-api-reference/#/searchResults /get_searchResults__id__relationships_playlists>`_: Search for playlists in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- playlists : `dict` A dictionary containing matching playlists. """ return self.search_relationship( query, "playlists", country_code, explicit=explicit, include=include, cursor=cursor, )
[docs] def search_top_hits( self, query: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Results > Search top hits <https://tidal-music.github.io /tidal-api-reference/#/searchResults /get_searchResults__id__relationships_topHits>`_: Search for top hits in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- top_hits : `dict` A dictionary containing matching top hits. """ return self.search_relationship( query, "topHits", country_code, explicit=explicit, include=include, cursor=cursor, )
[docs] def search_tracks( self, query: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Results > Search tracks <https://tidal-music.github.io /tidal-api-reference/#/searchResults /get_searchResults__id__relationships_tracks>`_: Search for tracks in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- tracks : `dict` A dictionary containing matching tracks. """ return self.search_relationship( query, "tracks", country_code, explicit=explicit, include=include, cursor=cursor, )
[docs] def search_videos( self, query: str, /, country_code: str, *, explicit: bool = True, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Results > Search videos <https://tidal-music.github.io /tidal-api-reference/#/searchResults /get_searchResults__id__relationships_videos>`_: Search for videos in the TIDAL catalog. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- videos : `dict` A dictionary containing matching videos. """ return self.search_relationship( query, "videos", country_code, explicit=explicit, include=include, cursor=cursor, )
### SEARCH SUGGESTIONS ####################################################
[docs] def get_search_suggestion( self, query: str, /, country_code: str, *, explicit: bool = True, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Search Suggestions > Get search suggestion <https://tidal-music.github.io/tidal-api-reference/# /searchSuggestions/get_searchSuggestions__id_>`_: Retrieve a single search suggestion for a given query. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. include : `str` or `list[str]`, keyword-only, optional Additional content types to include in the search suggestion. Returns ------- search_suggestion : `dict` A dictionary containing the search suggestion. """ self._check_authentication("get_search_suggestion") return self._get_json( f"{self.API_URL}/searchSuggestions/{urllib.parse.quote(query)}", params={ "countryCode": country_code, "explicitFilter": "include" if explicit else "exclude", "include": include, }, )
[docs] def get_search_direct_hits( self, query: str, /, country_code: str, *, explicit: bool = True, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Search Suggestions > Get search direct hits <https://tidal-music.github.io/tidal-api-reference/# /searchSuggestions /get_searchSuggestions__id__relationships_directHits>`_: Retrieve direct hits for search suggestions given a query. Parameters ---------- query : `str`, positional-only Search query. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. explicit : `bool`, keyword-only, default: :code:`True` Specifies whether to include explicit content in the search results. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- search_hits : `dict` A dictionary containing the search hits. """ self._check_authentication("get_search_suggestion_direct_hits") return self._get_json( f"{self.API_URL}/searchSuggestions" f"/{urllib.parse.quote(query)}/relationships/directHits", params={ "countryCode": country_code, "explicitFilter": "include" if explicit else "exclude", "include": "directHits", "page[cursor]": cursor, }, )
### TRACKS ################################################################
[docs] def get_track( self, track_id: str, /, country_code: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Tracks > Get single track <https://tidal-music.github.io /tidal-api-reference/#/tracks/get_tracks__id_>`_: Retrieve a single track by ID. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Example**: :code:`75413016` or :code:`"75413016"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"genres"`, :code:`"lyrics"`, :code:`"owners"`, :code:`"providers"`, :code:`"radio"`, :code:`"similarTracks"`, :code:`"sourceFile"`, and :code:`"trackStatistics"`. **Examples**: :code:`"albums"`, :code:`"albums,artists"`, and :code:`["albums", "artists"]`. Returns ------- track : `dict` TIDAL catalog information and related resources for a single track. """ if isinstance(include, str) and "," in include: include = include.split(",") return self._get_json( f"{self.API_URL}/tracks/{track_id}", params={"countryCode": country_code, "include": include}, )
[docs] def get_tracks( self, country_code: str, *, track_ids: Union[int, str, list[Union[int, str]], None] = None, isrcs: Union[str, list[str], None] = None, user_ids: Union[int, str, list[Union[int, str]], None] = None, include: Union[str, list[str], None] = None, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get multiple tracks <https://tidal-music.github.io /tidal-api-reference/#/tracks/get_tracks>`_: Retrieve multiple tracks by their IDs, ISRCs, or user IDs. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. track_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL track ID(s). **Examples**: :code:`75413016`, :code:`"75413016"`, and :code:`["46369325", "75413016"]`. isrcs : `int`, `str`, or `list`, keyword-only, optional International Standard Recording Code(s). **Examples**: :code:`"QMJMT1701237"`, :code:`"QMJMT1701237,QMJMT1701238"`, and :code:`["USUM72012345", "QMJMT1701238"]`. user_ids : `int`, `str`, or `list`, keyword-only, optional TIDAL user ID(s). **Example**: :code:`"123456"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"genres"`, :code:`"lyrics"`, :code:`"owners"`, :code:`"providers"`, :code:`"radio"`, :code:`"similarTracks"`, :code:`"sourceFile"`, and :code:`"trackStatistics"`. **Examples**: :code:`"albums"`, :code:`"albums,artists"`, and :code:`["albums", "artists"]`. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- tracks : `dict` A dictionary containing TIDAL catalog information and related resources for multiple tracks. """ if isinstance(include, str) and "," in include: include = include.split(",") if isinstance(user_ids, str) and "," in user_ids: user_ids = user_ids.split(",") if isinstance(isrcs, str) and "," in isrcs: isrcs = isrcs.split(",") if isinstance(track_ids, str) and "," in track_ids: track_ids = track_ids.split(",") return self._get_json( f"{self.API_URL}/tracks", params={ "countryCode": country_code, "page[cursor]": cursor, "include": include, "filter[r.owners.id]": user_ids, "filter[isrc]": isrcs, "filter[id]": track_ids, }, )
[docs] def get_track_relationship( self, track_id: Union[int, str], relationship: str, /, *, include: bool = False, **kwargs, ) -> dict[str, Any]: """ Retrieve information related to a track. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. relationship : `str`, positional-only Relationship type. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"owners"`, :code:`"providers"`, :code:`"radio"`, :code:`"similarTracks"`, :code:`"sourceFile"`, or :code:`"trackStatistics"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. **kwargs Keyword arguments. Returns ------- track_relationship : `dict` A dictionary containing TIDAL catalog information for the specified track relationship. """ return self._get_json( f"{self.API_URL}/tracks/{track_id}/relationships/{relationship}", params={"include": relationship if include else None, **kwargs}, )
[docs] def get_track_albums( self, track_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get track albums <https://tidal-music.github.io /tidal-api-reference/#/tracks /get_tracks__id__relationships_albums>`_: Retrieve albums containing a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- track_albums : `dict` A dictionary containing TIDAL catalog information for the albums containing the track. """ return self.get_track_relationship( track_id, "albums", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_track_artists( self, track_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get track artists <https://tidal-music.github.io /tidal-api-reference/#/tracks/get_tracks__id__relationships_artists>`_: Retrieve artists of a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- track_artists : `dict` A dictionary containing TIDAL catalog information for the artists of the track. """ return self.get_track_relationship( track_id, "artists", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_track_owners( self, track_id: Union[int, str], /, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get track owners <https://tidal-music.github.io /tidal-api-reference/#/tracks/get_tracks__id__relationships_owners>`_: Retrieve owners of a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- track_owners : `dict` A dictionary containing TIDAL catalog information for the owners of the track. """ return self.get_track_relationship( track_id, "owners", include=include, **{"page[cursor]": cursor} )
[docs] def get_track_providers( self, track_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get track providers <https://tidal-music.github.io /tidal-api-reference/#/tracks /get_tracks__id__relationships_providers>`_: Retrieve providers of a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- track_providers : `dict` A dictionary containing TIDAL catalog information for the providers of the track. """ return self.get_track_relationship( track_id, "providers", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_track_radio( self, track_id: Union[int, str], /, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get track radio <https://tidal-music.github.io /tidal-api-reference/#/tracks/get_tracks__id__relationships_radio>`_: Retrieve radio information for a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- track_radio : `dict` A dictionary containing TIDAL catalog information for the radio of the track. """ return self.get_track_relationship( track_id, "radio", include=include, **{"page[cursor]": cursor} )
[docs] def get_similar_tracks( self, track_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Tracks > Get similar tracks <https://tidal-music.github.io /tidal-api-reference/#/tracks /get_tracks__id__relationships_similarTracks>`_: Retrieve similar tracks for a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- similar_tracks : `dict` A dictionary containing TIDAL catalog information for the similar tracks. """ return self.get_track_relationship( track_id, "similar", countryCode=country_code, include=include, **{"page[cursor]": cursor}, )
[docs] def get_track_source_file( self, track_id: Union[int, str], /, *, include: bool = False ) -> dict[str, Any]: """ `Tracks > Get track source file <https://tidal-music.github.io /tidal-api-reference/#/tracks /get_tracks__id__relationships_sourceFile>`_: Retrieve source file information for a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. Returns ------- track_source_file : `dict` A dictionary containing TIDAL catalog information for the source file of the track. """ return self.get_track_relationship( track_id, "sourceFile", include=include )
[docs] def get_track_statistics( self, track_id: Union[int, str], /, *, include: bool = False ) -> dict[str, Any]: """ `Tracks > Get track statistics <https://tidal-music.github.io /tidal-api-reference/#/tracks /get_tracks__id__relationships_trackStatistics>`_: Retrieve statistics about a track. Parameters ---------- track_id : `int` or `str`, positional-only TIDAL track ID. **Examples**: :code:`75413016` and :code:`"75413016"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. Returns ------- track_statistics : `dict` A dictionary containing TIDAL catalog information for the statistics about the track. """ return self.get_track_relationship( track_id, "statistics", include=include )
### USER COLLECTIONS ######################################################
[docs] def get_user_collection( self, *, country_code: Union[str, None] = None, locale: Union[str, None] = None, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `User Collections > Get user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections/get_userCollections__id_>`_: Get a TIDAL user's collection. Parameters ---------- country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). include : str or Collection[str], keyword-only, optional Related resources to include in the response. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"owners"`, :code:`"playlists"`, :code:`"tracks"`, :code:`"videos"`. Returns ------- collection : dict[str, Any] TIDAL content metadata for the items in the current user's collection. """ self._check_scope("get_user_collection", "collection.read") return self._get_json( f"{self.API_URL}/userCollections/{self._user_id}", params={ "countryCode": country_code, "locale": locale, "include": include, }, )
[docs] def get_collection_relationship( self, relationship: str, /, *, country_code: str = None, locale: str = None, include: bool = False, cursor: str = None, sort: str = None, **kwargs, ) -> dict[str, Any]: """ Retrieve items in a user's collection. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- relationship : str, positional-only Relationship type. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. sort : str, keyword-only, optional Field to sort the returned albums by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. Returns ------- collection_relationship : `dict` A dictionary containing TIDAL catalog information for the specified collection relationship. """ self._check_scope("modify_collection_items", "collection.read") return self._get_json( f"{self.API_URL}/userCollections/{self._user_id}" f"/relationships/{relationship}", params={ "countryCode": country_code, "locale": locale, "include": relationship if include else None, "cursor": cursor, "sort": sort, **kwargs, }, )
[docs] def modify_collection_relationship( self, method: str, relationship: str, /, item_ids: Union[str, list[str]], *, country_code: Union[str, None] = None, ) -> None: """ Add/remove items of a relationship type to/from a user's collection. Parameters ---------- method : `str`, positional-only HTTP method. **Valid values**: :code:`"POST"`, :code:`"DELETE"`. relationship : `str`, positional-only Relationship type. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"owners"`, :code:`"playlists"`, :code:`"tracks"`, :code:`"videos"`. item_ids : `str` or `list` TIDAL IDs or UUIDs of items, provided as strings or properly formatted dictionaries. country_code : `str`, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self._check_scope("modify_collection_items", "collection.write") self._request( method, f"{self.API_URL}/userCollections/{self._user_id}" f"/relationships/{relationship}", params={"countryCode": country_code}, json={ "data": [ {"id": item_id, "type": relationship} for item_id in ( item_ids.split(",") if isinstance(str) else item_ids ) ] }, )
[docs] def get_saved_albums( self, *, country_code: str = None, locale: str = None, include: bool = False, cursor: str = None, sort: str = None, ) -> dict[str, Any]: """ `User Collections > Get albums in user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /get_userCollections__id__relationships_albums>`_: Get TIDAL catalog information for albums in a user's collection. Parameters ---------- country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for the albums in the user's collection. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. sort : str, keyword-only, optional Field to sort the returned albums by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. **Valid values**: :code:`"addedAt"`, :code:`"-addedAt`, :code:`"artists.name`, :code:`"-artists.name"`, :code:`"releaseDate"`, :code:`"-releaseDate"`, :code:`"title"`, :code:`"-title"`. Returns ------- albums : dict[str, Any] TIDAL content metadata for the albums in the user's collection. """ return self.get_collection_relationship( "albums", country_code=country_code, locale=locale, include=include, cursor=cursor, sort=sort, )
[docs] def save_albums( self, album_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Add albums to user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /post_userCollections__id__relationships_albums>`_: Add albums to a user's collection. Parameters ---------- album_ids : `str` or `list`, positional-only TIDAL album ID(s). **Examples**::code:`"251380836"`, :code:`"251380836,275646830"`, and :code:`["251380836", "275646830"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "POST", "albums", item_ids=album_ids, country_code=country_code )
[docs] def remove_saved_albums( self, album_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Remove albums from user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /delete_userCollections__id__relationships_albums>`_: Remove albums from a user's collection. Parameters ---------- album_ids : `str` or `list`, positional-only TIDAL album ID(s). **Examples**::code:`"251380836"`, :code:`"251380836,275646830"`, and :code:`["251380836", "275646830"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "DELETE", "albums", item_ids=album_ids, country_code=country_code )
[docs] def get_saved_artists( self, *, country_code: str = None, locale: str = None, include: bool = False, cursor: str = None, sort: str = None, ) -> dict[str, Any]: """ `User Collections > Get artists in user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /get_userCollections__id__relationships_artists>`_: Get TIDAL catalog information for artists in a user's collection. Parameters ---------- country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for the artists in the user's collection. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. sort : str, keyword-only, optional Field to sort the returned artists by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. **Valid values**: :code:`"addedAt"`, :code:`"-addedAt`, :code:`"name"`, :code:`"-name"`. Returns ------- artists : dict[str, Any] TIDAL content metadata for the artists in the user's collection. """ return self.get_collection_relationship( "artists", country_code=country_code, locale=locale, include=include, cursor=cursor, sort=sort, )
[docs] def save_artists( self, artist_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Add artists to user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /post_userCollections__id__relationships_artists>`_: Add artists to a user's collection. Parameters ---------- artist_ids : `str` or `list`, positional-only TIDAL artist ID(s). **Examples**: :code:`1`, :code:`"1"`, :code:`"1,2"`, :code:`[1, 2]`, and :code:`["1", "2"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "POST", "artists", item_ids=artist_ids, country_code=country_code )
[docs] def remove_saved_artists( self, artist_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Remove artists from user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /delete_userCollections__id__relationships_artists>`_: Remove artists from a user's collection. Parameters ---------- artist_ids : `str` or `list`, positional-only TIDAL artist ID(s). **Examples**: :code:`1`, :code:`"1"`, :code:`"1,2"`, :code:`[1, 2]`, and :code:`["1", "2"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "DELETE", "artists", item_ids=artist_ids, country_code=country_code )
[docs] def get_saved_owners( self, include: bool = False, cursor: Union[str, None] = None ) -> dict[str, Any]: """ `User Collections > Get owners of user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /get_userCollections__id__relationships_owners>`_: Get TIDAL catalog information for owners of a user's collection. Parameters ---------- include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for the owners of the user's collection. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. Returns ------- owners : dict[str, Any] TIDAL content metadata for the owners of the user's collection. """ return self.get_collection_relationship( "owners", include=include, cursor=cursor )
[docs] def get_saved_playlists( self, *, country_code: str = None, locale: str = None, folders: bool = False, include: bool = False, cursor: str = None, sort: str = None, ) -> dict[str, Any]: """ `User Collections > Get playlists in user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /get_userCollections__id__relationships_playlists>`_: Get TIDAL catalog information for playlists in a user's collection. Parameters ---------- country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). folders : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for playlist folders in the user's collection. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for the playlists in the user's collection. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. sort : str, keyword-only, optional Field to sort the returned playlists by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. **Valid values**: :code:`"addedAt"`, :code:`"-addedAt`, :code:`"lastUpdatedAt"`, :code:`"-lastUpdatedAt"`, :code:`"name"`, :code:`"-name"`. Returns ------- playlists : dict[str, Any] TIDAL content metadata for the playlists (and playlist folders) in the user's collection. """ return self.get_collection_relationship( "playlists", country_code=country_code, locale=locale, include=include, cursor=cursor, sort=sort, collectionView="FOLDERS" if folders else None, )
[docs] def save_playlists( self, playlist_uuids: Union[str, list[str]], /, ) -> None: """ `User Collections > Add playlists to user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /post_userCollections__id__relationships_playlists>`_: Add playlists to a user's collection. Parameters ---------- playlist_uuids : `str` or `list`, positional-only, optional TIDAL playlist UUID(s). **Examples**: :code:`"550e8400-e29b-41d4-a716-446655440000"` and :code:`["550e8400-e29b-41d4-a716-446655440000", "4261748a-4287-4758-aaab-6d5be3e99e52"]`. """ self.modify_collection_relationship( "POST", "playlists", item_ids=playlist_uuids )
[docs] def remove_saved_playlists( self, playlist_uuids: Union[str, list[str]], /, ) -> None: """ `User Collections > Remove playlists from user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /delete_userCollections__id__relationships_playlists>`_: Remove playlists from a user's collection. Parameters ---------- playlist_uuids : `str` or `list`, positional-only, optional TIDAL playlist UUID(s). **Examples**: :code:`"550e8400-e29b-41d4-a716-446655440000"` and :code:`["550e8400-e29b-41d4-a716-446655440000", "4261748a-4287-4758-aaab-6d5be3e99e52"]`. """ self.modify_collection_relationship( "DELETE", "playlists", item_ids=playlist_uuids )
[docs] def get_saved_tracks( self, *, country_code: str = None, locale: str = None, include: bool = False, cursor: str = None, sort: str = None, ) -> dict[str, Any]: """ `User Collections > Get tracks in user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /get_userCollections__id__relationships_tracks>`_: Get TIDAL catalog information for tracks in a user's collection. Parameters ---------- country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for the tracks in the user's collection. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. sort : str, keyword-only, optional Field to sort the returned tracks by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. **Valid values**: :code:`"addedAt"`, :code:`"-addedAt`, :code:`"albums.title"`, :code:`"-albums.title"`, :code:`"artists.name"`, :code:`"-artists.name"`, :code:`"duration"`, :code:`"-duration"`, :code:`"title"`, :code:`"-title"`. Returns ------- tracks : dict[str, Any] TIDAL content metadata for the tracks in the user's collection. """ return self.get_collection_relationship( "tracks", country_code=country_code, locale=locale, include=include, cursor=cursor, sort=sort, )
[docs] def save_tracks( self, track_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Add tracks to user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /post_userCollections__id__relationships_tracks>`_: Add tracks to a user's collection. Parameters ---------- track_ids : `str` or `list`, positional-only TIDAL track ID(s). **Examples**: :code:`"46369325"`, :code:`["46369325", "75413016"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "POST", "tracks", item_ids=track_ids, country_code=country_code )
[docs] def remove_saved_tracks( self, track_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Remove tracks from user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /delete_userCollections__id__relationships_tracks>`_: Remove tracks from a user's collection. Parameters ---------- track_ids : `str` or `list`, positional-only TIDAL track ID(s). **Examples**: :code:`"46369325"`, :code:`["46369325", "75413016"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "DELETE", "tracks", item_ids=track_ids, country_code=country_code )
[docs] def get_saved_videos( self, *, country_code: str = None, locale: str = None, include: bool = False, cursor: str = None, sort: str = None, ) -> dict[str, Any]: """ `User Collections > Get videos in user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /get_userCollections__id__relationships_videos>`_: Get TIDAL catalog information for videos in a user's collection. Parameters ---------- country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : str, keyword-only, optional IETF BCP 47 language tag. **Default**: :code:`"en_US"` – English (U.S.). include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata for the videos in the user's collection. cursor : str, keyword-only, optional Cursor for pagination. **Example**: :code:`"3nI1Esi"`. sort : str, keyword-only, optional Field to sort the returned videos by. Values are sorted in descending order with the :code:`-` prefix and in ascending order without. **Valid values**: :code:`"addedAt"`, :code:`"-addedAt`, :code:`"artists.name"`, :code:`"-artists.name"`, :code:`"duration"`, :code:`"-duration"`, :code:`"title"`, :code:`"-title"`. Returns ------- videos : dict[str, Any] TIDAL content metadata for the videos in the user's collection. """ return self.get_collection_relationship( "videos", country_code=country_code, locale=locale, include=include, cursor=cursor, sort=sort, )
[docs] def save_videos( self, video_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Add videos to user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /post_userCollections__id__relationships_videos>`_: Add videos to a user's collection. Parameters ---------- video_ids : `str` or `list`, positional-only TIDAL video ID(s). **Examples**: :code:`75623239`, :code:`"75623239"`, and :code:`["75623239", "75623240"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "POST", "videos", item_ids=video_ids, country_code=country_code )
[docs] def remove_saved_videos( self, video_ids: Union[str, list[str]], /, *, country_code: str = None, ) -> None: """ `User Collections > Remove videos from user's collection <https://tidal-music.github.io/tidal-api-reference/# /userCollections /delete_userCollections__id__relationships_videos>`_: Remove videos from a user's collection. Parameters ---------- video_ids : `str` or `list`, positional-only TIDAL video ID(s). **Examples**: :code:`75623239`, :code:`"75623239"`, and :code:`["75623239", "75623240"]`. country_code : str, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ self.modify_collection_relationship( "DELETE", "videos", item_ids=video_ids, country_code=country_code )
### USER ENTITLEMENTS #####################################################
[docs] def get_user_entitlements(self) -> dict[str, Any]: """ `User Entitlements > Get user entitlements <https://tidal-music.github.io/tidal-api-reference/# /userEntitlements/get_userEntitlements__id_>`_: Retrieve user entitlements. Returns ------- user_entitlements : `dict` A dictionary containing user entitlements. """ self._check_scope("get_user_entitlements", "entitlements.read") return self._get_json( f"{self.API_URL}/userEntitlements/{self._user_id}" )
### USER RECOMMENDATIONS ##################################################
[docs] def get_user_recommendations( self, country_code: str, locale: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Users > Get user recommendations <https://tidal-music.github.io /tidal-api-reference/#/userRecommendations /get_userRecommendations__id_>`_: Retrieve user recommendations. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : `str` IETF BCP 47 language tag. **Example**: :code:`"en-US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"discoveryMixes"`, :code:`"myMixes"`, and :code:`"newArrivalMixes"`. **Examples**: :code:`"discoveryMixes"`, :code:`"discoveryMixes,myMixes"`, and :code:`["discoveryMixes", "myMixes"]`. Returns ------- user_recommendations : `dict` A dictionary containing TIDAL catalog information for the user recommendations. """ self._check_scope("get_user_recommendations", "recommendations.read") return self._get_json( f"{self.API_URL}/userRecommendations/{self._user_id}", params={ "countryCode": country_code, "locale": locale, "include": include, }, )
[docs] def get_mixes( self, mix_type: str, /, country_code: str, locale: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ Retrieve mixes of a specific type for a user. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- user_id : `int` or `str`, positional-only TIDAL user ID. **Examples**: :code:`123456` and :code:`"123456"`. mix_type : `str`, positional-only Mix type. **Valid values**: :code:`"discovery"`, :code:`"my"`, and :code:`"newArrival"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : `str` IETF BCP 47 language tag. **Example**: :code:`"en-US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, optional Pagination cursor. Returns ------- mixes : `dict` A dictionary containing TIDAL catalog information for the user's mixes. """ return self._get_json( f"{self.API_URL}/userRecommendations/{self._user_id}/{mix_type}Mixes", params={ "countryCode": country_code, "locale": locale, "include": f"{mix_type}Mixes" if include else None, "page[cursor]": cursor, }, )
[docs] def get_discovery_mixes( self, country_code: str, locale: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `User Recommendations > Get discovery mixes <https://tidal-music.github.io/tidal-api-reference/# /userRecommendations /get_userRecommendations__id__relationships_discoveryMixes>`_: Retrieve discovery mixes for a user. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : `str` IETF BCP 47 language tag. **Example**: :code:`"en-US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. """ self._check_scope("get_discovery_mixes", "recommendations.read") return self.get_mixes( "discovery", country_code, locale, include=include, cursor=cursor )
[docs] def get_user_mixes( self, country_code: str, locale: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `User Recommendations > Get user mixes <https://tidal-music.github.io/tidal-api-reference/# /userRecommendations /get_userRecommendations__id__relationships_myMixes>`_: Retrieve user mixes for a user. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : `str` IETF BCP 47 language tag. **Example**: :code:`"en-US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. """ self._check_scope("get_user_mixes", "recommendations.read") return self.get_mixes( "my", country_code, locale, include=include, cursor=cursor )
[docs] def get_new_arrival_mixes( self, country_code: str, locale: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `User Recommendations > Get new arrival mixes <https://tidal-music.github.io/tidal-api-reference/# /userRecommendations /get_userRecommendations__id__relationships_newArrivalMixes>`_: Retrieve new arrival mixes for a user. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. locale : `str` IETF BCP 47 language tag. **Example**: :code:`"en-US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. """ self._check_scope("get_new_arrival_mixes", "recommendations.read") return self.get_mixes( "newArrival", country_code, locale, include=include, cursor=cursor )
### USERS #################################################################
[docs] def get_me(self) -> dict[str, Any]: """ `Users > Get current user <https://tidal-music.github.io /tidal-api-reference/#/users/get_users_me>`_: Retrieve information about the authenticated user. Returns ------- user_info : `dict` A dictionary containing TIDAL catalog information for the authenticated user. """ return self._get_json(f"{self.API_URL}/users/me")
### VIDEOS ################################################################
[docs] def get_video( self, video_id: str, /, country_code: str, *, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Videos > Get single video <https://tidal-music.github.io /tidal-api-reference/#/videos/get_videos__id_>`_: Retrieve a single video by ID. Parameters ---------- video_id : `int` or `str`, positional-only TIDAL video ID. **Example**: :code:`75623239` or :code:`"75623239"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"providers"`, and :code:`"thumbnailArt"`. **Examples**: :code:`"albums"`, :code:`"albums,artists"`, and :code:`["albums", "artists"]`. Returns ------- video : `dict` TIDAL catalog information and related resources for a single video. """ if isinstance(include, str) and "," in include: include = include.split(",") return self._get_json( f"{self.API_URL}/videos/{video_id}", params={"countryCode": country_code, "include": include}, )
[docs] def get_videos( self, country_code: str, *, video_ids: Union[int, str, list[Union[int, str]], None] = None, isrcs: Union[str, list[str], None] = None, include: Union[str, list[str], None] = None, ) -> dict[str, Any]: """ `Videos > Get multiple videos <https://tidal-music.github.io /tidal-api-reference/#/videos/get_videos>`_: Retrieve multiple videos by their IDs or ISRCs. Parameters ---------- country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. video_ids : `int`, `str` or `list`, keyword-only, optional TIDAL video ID(s). **Examples**: :code:`75623239`, :code:`"75623239"`, and :code:`["75623239", "75623240"]`. isrcs : `int` or `str` or `list`, keyword-only, optional International Standard Recording Code(s) (ISRC) for the video(s). **Examples**: :code:`"USSM21600755"`, :code:`"USSM21600755,USSM21600756"`, and :code:`["USSM21600755", "USSM21600756"]`. include : `str` or `list`, keyword-only, optional Related resource(s) that should be included in the response. **Valid values**: :code:`"albums"`, :code:`"artists"`, :code:`"providers"`, and :code:`"thumbnailArt"`. **Examples**: :code:`"albums"`, :code:`"albums,artists"`, and :code:`["albums", "artists"]`. Returns ------- videos : `dict` TIDAL catalog information and related resources for multiple videos. """ if isinstance(include, str) and "," in include: include = include.split(",") if isinstance(isrcs, str) and "," in isrcs: isrcs = isrcs.split(",") if isinstance(video_ids, str) and "," in video_ids: video_ids = video_ids.split(",") return self._get_json( f"{self.API_URL}/videos", params={ "countryCode": country_code, "include": include, "filter[isrc]": isrcs, "filter[id]": video_ids, }, )
[docs] def get_video_relationship( self, video_id: Union[int, str], relationship: str, /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ Retrieve information related to a video. .. note:: This method is provided for convenience and is not a TIDAL API endpoint. Parameters ---------- video_id : `int` or `str`, positional-only TIDAL video ID. **Examples**: :code:`75623239` and :code:`"75623239"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- relationship : `dict` A dictionary containing TIDAL catalog information for the specified video relationship. """ return self._get_json( f"{self.API_URL}/videos/{video_id}/relationships", params={ "countryCode": country_code, "include": relationship if include else None, "page[cursor]": cursor, }, )
[docs] def get_video_albums( self, video_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Videos > Get video albums <https://tidal-music.github.io /tidal-api-reference/#/videos /get_videos__id__relationships_albums>`_: Retrieve albums containing a video. Parameters ---------- video_id : `int` or `str`, positional-only TIDAL video ID. **Examples**: :code:`75623239` and :code:`"75623239"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- video_albums : `dict` A dictionary containing TIDAL catalog information for the albums containing the video. """ return self.get_video_relationship( video_id, "albums", country_code=country_code, include=include, cursor=cursor, )
[docs] def get_video_artists( self, video_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Videos > Get video artists <https://tidal-music.github.io /tidal-api-reference/#/videos /get_videos__id__relationships_artists>`_: Retrieve artists associated with a video. Parameters ---------- video_id : `int` or `str`, positional-only TIDAL video ID. **Examples**: :code:`75623239` and :code:`"75623239"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- video_artists : `dict` A dictionary containing TIDAL catalog information for the artists associated with the video. """ return self.get_video_relationship( video_id, "artists", country_code=country_code, include=include, cursor=cursor, )
[docs] def get_video_providers( self, video_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Videos > Get video providers <https://tidal-music.github.io /tidal-api-reference/#/videos /get_videos__id__relationships_providers>`_: Retrieve providers of a video. Parameters ---------- video_id : `int` or `str`, positional-only TIDAL video ID. **Examples**: :code:`75623239` and :code:`"75623239"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- video_providers : `dict` A dictionary containing TIDAL catalog information for the providers of the video. """ return self.get_video_relationship( video_id, "providers", country_code=country_code, include=include, cursor=cursor, )
[docs] def get_video_thumbnail_art( self, video_id: Union[int, str], /, country_code: str, *, include: bool = False, cursor: Union[int, str, None] = None, ) -> dict[str, Any]: """ `Videos > Get video thumbnail art <https://tidal-music.github.io /tidal-api-reference/#/videos /get_videos__id__relationships_thumbnailArt>`_: Retrieve thumbnail art for a video. Parameters ---------- video_id : `int` or `str`, positional-only TIDAL video ID. **Examples**: :code:`75623239` and :code:`"75623239"`. country_code : `str` ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. include : bool, keyword-only, default: :code:`False` Specifies whether to include TIDAL content metadata in the response. cursor : `int` or `str`, keyword-only, optional Pagination cursor. Returns ------- video_thumbnail_art : `dict` A dictionary containing TIDAL catalog information for the thumbnail art for the video. """ return self.get_video_relationship( video_id, "thumbnailArt", country_code=country_code, include=include, cursor=cursor, )
[docs] class PrivateAPI: """ Private TIDAL API client. The private TIDAL API allows media (tracks, videos), collections (albums, playlists), and performers to be queried, and information about them to be retrieved. As there is no available official documentation for the private TIDAL API, its endpoints have been determined by watching HTTP network traffic. .. attention:: As the private TIDAL API is not designed to be publicly accessible, this class can be disabled or removed at any time to ensure compliance with the `TIDAL Developer Terms of Service <https://developer.tidal.com/documentation/guidelines /guidelines-developer-terms>`_. While authentication is not necessary to search for and retrieve data from public content, it is required to access personal content and stream media (with an active TIDAL subscription). In the latter case, requests to the private TIDAL API endpoints must be accompanied by a valid user access token in the header. Minim can obtain user access tokens via the authorization code with proof key for code exchange (PKCE) and device code flows. These OAuth 2.0 authorization flows require valid client credentials (client ID and client secret) to either be provided to this class's constructor as keyword arguments or be stored as :code:`TIDAL_PRIVATE_CLIENT_ID` and :code:`TIDAL_PRIVATE_CLIENT_SECRET` in the operating system's environment variables. .. hint:: Client credentials can be extracted from the software you use to access TIDAL, including but not limited to the TIDAL Web Player and the Android, iOS, macOS, and Windows applications. Only the TIDAL Web Player and desktop application client credentials can be used without authorization. If an existing access token is available, it and its accompanying information (refresh token and expiry time) can be provided to this class's constructor as keyword arguments to bypass the access token retrieval process. It is recommended that all other authorization-related keyword arguments be specified so that a new access token can be obtained when the existing one expires. .. tip:: The authorization flow and access token can be changed or updated at any time using :meth:`set_flow` and :meth:`set_access_token`, respectively. Minim also stores and manages access tokens and their properties. When an access token is acquired, it is automatically saved to the Minim configuration file to be loaded on the next instantiation of this class. This behavior can be disabled if there are any security concerns, like if the computer being used is a shared device. Parameters ---------- client_id : `str`, keyword-only, optional Client ID. If it is not stored as :code:`TIDAL_PRIVATE_CLIENT_ID` in the operating system's environment variables or found in the Minim configuration file, it must be provided here. client_secret : `str`, keyword-only, optional Client secret. Required for the authorization code and device code flows. If it is not stored as :code:`TIDAL_PRIVATE_CLIENT_SECRET` in the operating system's environment variables or found in the Minim configuration file, it must be provided here. flow : `str`, keyword-only, optional Authorization flow. If not specified, no user authorization will be performed. .. container:: **Valid values**: * :code:`"pkce"` for the authorization code with proof key for code exchange (PKCE) flow. * :code:`"device_code"` for the device code flow. browser : `bool`, keyword-only, default: :code:`False` Determines whether a web browser is automatically opened for the authorization code with PKCE or device code flows. If :code:`False`, users will have to manually open the authorization URL, and for the authorization code flow, provide the full callback URI via the terminal. For the authorization code with PKCE flow, the Playwright framework by Microsoft is used. scopes : `str` or `list`, keyword-only, default: :code:`"r_usr"` Authorization scopes to request user access for in the OAuth 2.0 flows. **Valid values**: :code:`"r_usr"`, :code:`"w_usr"`, and :code:`"w_sub"` (device code flow only). user_agent : `str`, keyword-only, optional User agent information to send in the header of HTTP requests. .. note:: If not specified, TIDAL may temporarily block your IP address if you are making requests too quickly. access_token : `str`, keyword-only, optional Access token. If provided here or found in the Minim configuration file, the authorization process is bypassed. In the former case, all other relevant keyword arguments should be specified to automatically refresh the access token when it expires. refresh_token : `str`, keyword-only, optional Refresh token accompanying `access_token`. If not provided, the user will be reauthenticated using the specified authorization flow when `access_token` expires. expiry : `datetime.datetime` or `str`, keyword-only, optional Expiry time of `access_token` in the ISO 8601 format :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be reauthenticated using `refresh_token` (if available) or the specified authorization flow (if possible) when `access_token` expires. overwrite : `bool`, keyword-only, default: :code:`False` Determines whether to overwrite an existing access token in the Minim configuration file. save : `bool`, keyword-only, default: :code:`True` Determines whether newly obtained access tokens and their associated properties are stored to the Minim configuration file. Attributes ---------- API_URL : `str` Base URL for the private TIDAL API. AUTH_URL : `str` URL for device code requests. LOGIN_URL : `str` URL for authorization code requests. REDIRECT_URL : `str` URL for authorization code callbacks. RESOURCES_URL : `str` URL for cover art and image requests. TOKEN_URL : `str` URL for access token requests. WEB_URL : `str` URL for the TIDAL Web Player. session : `requests.Session` Session used to send requests to the private TIDAL API. """ _FLOWS = {"pkce", "device_code"} _NAME = f"{__module__}.{__qualname__}" API_URL = "https://api.tidal.com" AUTH_URL = "https://auth.tidal.com/v1/oauth2" LOGIN_URL = "https://login.tidal.com" REDIRECT_URI = "tidal://login/auth" RESOURCES_URL = "https://resources.tidal.com" WEB_URL = "https://listen.tidal.com" def __init__( self, *, client_id: str = None, client_secret: str = None, flow: str = None, browser: bool = False, scopes: Union[str, list[str]] = "r_usr", user_agent: str = None, access_token: str = None, refresh_token: str = None, expiry: datetime.datetime = None, overwrite: bool = False, save: bool = True, ) -> None: """ Create a private TIDAL API client. """ self.session = requests.Session() if user_agent: self.session.headers["User-Agent"] = user_agent if ( access_token is None and _config.has_section(self._NAME) and not overwrite ): flow = _config.get(self._NAME, "flow") access_token = _config.get(self._NAME, "access_token") refresh_token = _config.get(self._NAME, "refresh_token") expiry = _config.get(self._NAME, "expiry") client_id = _config.get(self._NAME, "client_id") client_secret = _config.get(self._NAME, "client_secret") scopes = _config.get(self._NAME, "scopes") self.set_flow( flow, client_id=client_id, client_secret=client_secret, browser=browser, scopes=scopes, save=save, ) self.set_access_token( access_token, refresh_token=refresh_token, expiry=expiry ) def _check_scope( self, endpoint: str, scope: str = None, *, flows: Union[str, list[set], set[str]] = None, require_authentication: bool = True, ) -> None: """ Check if the user has granted the appropriate authorization scope for the desired endpoint. Parameters ---------- endpoint : `str` Private TIDAL API endpoint. scope : `str`, optional Required scope for `endpoint`. flows : `str`, `list`, or `set`, keyword-only, optional Authorization flows for which `scope` is required. If not specified, `flows` defaults to all supported authorization flows. require_authentication : `bool`, keyword-only, default: :code:`True` Specifies whether the endpoint requires user authentication. Some endpoints can be used without authentication but require specific scopes when user authentication has been performed. """ if flows is None: flows = self._FLOWS if require_authentication: if self._flow is None: emsg = ( f"{self._NAME}.{endpoint}() requires user authentication." ) elif self._flow in flows and scope and scope not in self._scopes: emsg = ( f"{self._NAME}.{endpoint}() requires the '{scope}' " "authorization scope." ) else: return elif self._flow in flows and scope and scope not in self._scopes: emsg = ( f"{self._NAME}.{endpoint}() requires the '{scope}' " "authorization scope when user authentication has " f"been performed via the '{self._flow}' " "authorization flow." ) else: return raise RuntimeError(emsg) def _get_authorization_code(self, code_challenge: str) -> str: """ Get an authorization code to be exchanged for an access token in the authorization code flow. Parameters ---------- code_challenge : `str`, optional Code challenge for the authorization code with PKCE flow. Returns ------- auth_code : `str` Authorization code. """ params = { "client_id": self._client_id, "code_challenge": code_challenge, "code_challenge_method": "S256", "redirect_uri": self.REDIRECT_URI, "response_type": "code", } if self._scopes: params["scope"] = self._scopes auth_url = ( f"{self.LOGIN_URL}/authorize?{urllib.parse.urlencode(params)}" ) if self._browser: har_file = DIR_TEMP / "minim_tidal_private.har" with sync_playwright() as playwright: browser = playwright.firefox.launch(headless=False) context = browser.new_context( locale="en-US", timezone_id="America/Los_Angeles", record_har_path=har_file, **playwright.devices["Desktop Firefox HiDPI"], ) page = context.new_page() page.goto(auth_url, timeout=0) page.wait_for_url(f"{self.REDIRECT_URI}*", wait_until="commit") context.close() browser.close() with open(har_file, "r") as f: queries = dict( urllib.parse.parse_qsl( urllib.parse.urlparse( re.search( rf'{self.REDIRECT_URI}\?(.*?)"', f.read() ).group(0) ).query ) ) har_file.unlink() else: print( "To grant Minim access to TIDAL data and features, " "open the following link in your web browser:\n\n" f"{auth_url}\n" ) uri = input( "After authorizing Minim to access TIDAL on " "your behalf, copy and paste the URI beginning " f"with '{self.REDIRECT_URI}' below.\n\nURI: " ) queries = dict( urllib.parse.parse_qsl(urllib.parse.urlparse(uri).query) ) if "code" not in queries: raise RuntimeError("Authorization failed.") return queries["code"] def _get_country_code(self, country_code: str = None) -> str: """ Get the ISO 3166-1 alpha-2 country code to use for requests. Parameters ---------- country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- country_code : `str` ISO 3166-1 alpha-2 country code. """ return ( country_code or getattr(self, "_country_code", None) or self.get_country_code() ) def _get_json(self, url: str, **kwargs) -> dict: """ Send a GET request and return the JSON-encoded content of the response. Parameters ---------- url : `str` URL for the GET request. **kwargs Keyword arguments to pass to :meth:`requests.request`. Returns ------- resp : `dict` JSON-encoded content of the response. """ return self._request("get", url, **kwargs).json() def _refresh_access_token(self) -> None: """ Refresh the expired excess token. """ if ( self._flow is None or not self._refresh_token or not self._client_id or (self._flow == "device_code" and not self._client_secret) ): self.set_access_token() else: r = requests.post( f"{self.LOGIN_URL}/oauth2/token", data={ "client_id": self._client_id, "client_secret": self._client_secret, "grant_type": "refresh_token", "refresh_token": self._refresh_token, }, ).json() self.session.headers["Authorization"] = ( f"Bearer {r['access_token']}" ) self._expiry = datetime.datetime.now() + datetime.timedelta( 0, r["expires_in"] ) self._scopes = r["scope"] if self._save: _config[self._NAME].update( { "access_token": r["access_token"], "expiry": self._expiry.strftime("%Y-%m-%dT%H:%M:%SZ"), "scopes": self._scopes, } ) with open(DIR_HOME / "minim.cfg", "w") as f: _config.write(f) def _request( self, method: str, url: str, retry: bool = True, **kwargs ) -> requests.Response: """ Construct and send a request with status code checking. Parameters ---------- method : `str` Method for the request. url : `str` URL for the request. retry : `bool` Specifies whether to retry the request if the response has a non-2xx status code. **kwargs Keyword arguments passed to :meth:`requests.request`. Returns ------- resp : `requests.Response` Response to the request. """ if self._expiry is not None and datetime.datetime.now() > self._expiry: self._refresh_access_token() r = self.session.request(method, url, **kwargs) if r.status_code not in range(200, 299): if r.text: error = r.json() substatus = ( error["subStatus"] if "subStatus" in error else error["sub_status"] if "sub_status" in error else "" ) description = ( error["userMessage"] if "userMessage" in error else ( error["description"] if "description" in error else ( error["error_description"] if "error_description" in error else "" ) ) ) emsg = f"{r.status_code}" if substatus: emsg += f".{substatus}" emsg += f" {description}" else: emsg = f"{r.status_code} {r.reason}" if r.status_code == 401 and substatus == 11003 and retry: logging.warning(emsg) self._refresh_access_token() return self._request(method, url, False, **kwargs) else: raise RuntimeError(emsg) return r
[docs] def set_access_token( self, access_token: str = None, *, refresh_token: str = None, expiry: Union[str, datetime.datetime] = None, ) -> None: """ Set the private TIDAL API access token. Parameters ---------- access_token : `str`, optional Access token. If not provided, an access token is obtained using an OAuth 2.0 authorization flow. refresh_token : `str`, keyword-only, optional Refresh token accompanying `access_token`. expiry : `str` or `datetime.datetime`, keyword-only, optional Access token expiry timestamp in the ISO 8601 format :code:`%Y-%m-%dT%H:%M:%SZ`. If provided, the user will be reauthenticated using the refresh token (if available) or the default authorization flow (if possible) when `access_token` expires. """ if access_token is None: if self._flow is None: self._expiry = datetime.datetime.max return else: if not self._client_id: emsg = "Private TIDAL API client ID not provided." raise ValueError(emsg) if self._flow == "pkce": data = { "client_id": self._client_id, "code_verifier": secrets.token_urlsafe(32), "grant_type": "authorization_code", "redirect_uri": self.REDIRECT_URI, "scope": self._scopes, } data["code"] = self._get_authorization_code( base64.urlsafe_b64encode( hashlib.sha256( data["code_verifier"].encode() ).digest() ) .decode() .rstrip("=") ) r = requests.post( f"{self.LOGIN_URL}/oauth2/token", json=data ).json() elif self._flow == "device_code": if not self._client_secret: emsg = "Private TIDAL API client secret not provided." raise ValueError(emsg) data = {"client_id": self._client_id} if self._scopes: data["scope"] = self._scopes r = requests.post( f"{self.AUTH_URL}/device_authorization", data=data ).json() if "error" in r: emsg = ( f"{r['status']}.{r['sub_status']} " f"{r['error_description']}" ) raise ValueError(emsg) data["device_code"] = r["deviceCode"] data["grant_type"] = ( "urn:ietf:params:oauth:grant-type:device_code" ) verification_uri = f"http://{r['verificationUriComplete']}" if self._browser: webbrowser.open(verification_uri) else: print( "To grant Minim access to TIDAL data and " "features, open the following link in " f"your web browser:\n\n{verification_uri}\n" ) while True: time.sleep(2) r = requests.post( f"{self.AUTH_URL}/token", auth=(self._client_id, self._client_secret), data=data, ).json() if "error" not in r: break elif r["error"] != "authorization_pending": raise RuntimeError( f"{r['status']}.{r['sub_status']} " f"{r['error_description']}" ) access_token = r["access_token"] refresh_token = r["refresh_token"] expiry = datetime.datetime.now() + datetime.timedelta( 0, r["expires_in"] ) if self._save: _config[self._NAME] = { "flow": self._flow, "client_id": self._client_id, "access_token": access_token, "refresh_token": refresh_token, "expiry": expiry.strftime("%Y-%m-%dT%H:%M:%SZ"), "scopes": self._scopes, } if hasattr(self, "_client_secret"): _config[self._NAME]["client_secret"] = ( self._client_secret ) with open(DIR_HOME / "minim.cfg", "w") as f: _config.write(f) if len(access_token) == 16: self.session.headers["x-tidal-token"] = access_token self._refresh_token = self._expiry = None else: self.session.headers["Authorization"] = f"Bearer {access_token}" self._refresh_token = refresh_token self._expiry = ( datetime.datetime.strptime(expiry, "%Y-%m-%dT%H:%M:%SZ") if isinstance(expiry, str) else expiry ) if self._flow is not None: me = self.get_profile() self._country_code = me["countryCode"] self._user_id = me["userId"]
[docs] def set_flow( self, flow: str, client_id: str, *, client_secret: str = None, browser: bool = False, scopes: Union[str, list[str]] = "", save: bool = True, ) -> None: """ Set the authorization flow. Parameters ---------- flow : `str` Authorization flow. If not specified, no user authentication will be performed. .. container:: **Valid values**: * :code:`"pkce"` for the authorization code with proof key for code exchange (PKCE) flow. * :code:`"client_credentials"` for the client credentials flow. client_id : `str` Client ID. client_secret : `str`, keyword-only, optional Client secret. Required for all OAuth 2.0 authorization flows. browser : `bool`, keyword-only, default: :code:`False` Determines whether a web browser is automatically opened for the authorization code with PKCE or device code flows. If :code:`False`, users will have to manually open the authorization URL, and for the authorization code flow, provide the full callback URI via the terminal. For the authorization code with PKCE flow, the Playwright framework by Microsoft is used. scopes : `str` or `list`, keyword-only, optional Authorization scopes to request user access for in the OAuth 2.0 flows. **Valid values**: :code:`"r_usr"`, :code:`"w_usr"`, and :code:`"w_sub"` (device code flow only). save : `bool`, keyword-only, default: :code:`True` Determines whether to save the newly obtained access tokens and their associated properties to the Minim configuration file. """ if flow and flow not in self._FLOWS: emsg = ( f"Invalid authorization flow ({flow=}). " f"Valid values: {', '.join(self._FLOWS)}." ) raise ValueError(emsg) self._flow = flow self._save = save self._client_id = client_id or os.environ.get( "TIDAL_PRIVATE_CLIENT_ID" ) if flow: if "x-tidal-token" in self.session.headers: del self.session.headers["x-tidal-token"] self._browser = browser if flow == "pkce" and browser and not FOUND_PLAYWRIGHT: self._browser = False wmsg = ( "The Playwright web framework was not found, " "so automatic authorization code retrieval is " "not available." ) warnings.warn(wmsg) self._client_secret = client_secret or os.environ.get( "TIDAL_PRIVATE_CLIENT_SECRET" ) self._scopes = ( " ".join(scopes) if isinstance(scopes, list) else scopes ) else: self.session.headers["x-tidal-token"] = self._client_id self._scopes = ""
### ALBUMS ################################################################
[docs] def get_album( self, album_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for an album. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_id : `int` or `str` TIDAL album ID. **Example**: :code:`251380836`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- album : `dict` TIDAL catalog information for an album. .. admonition:: Sample response :class: dropdown .. code:: { "id": <int>, "title": <str>, "duration": <int>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "premiumStreamingOnly": <bool>, "numberOfTracks": <int>, "numberOfVideos": <int>, "numberOfVolumes": <int>, "releaseDate": <str>, "copyright": <str>, "type": "ALBUM", "version": <str>, "url": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str>, "explicit": <bool>, "upc": <str>, "popularity": <int>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ] } """ self._check_scope( "get_album", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/albums/{album_id}", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_album_items( self, album_id: Union[int, str], country_code: str = None, *, limit: int = 100, offset: int = None, credits: bool = False, ) -> dict[str, Any]: """ Get TIDAL catalog information for items (tracks and videos) in an album. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_id : `int` or `str` TIDAL album ID. **Examples**: :code:`251380836`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. credits : `bool`, keyword-only, default: :code:`False` Determines whether credits for each item is returned. Returns ------- items : `dict` A dictionary containing TIDAL catalog information for tracks and videos in the specified album and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "item": { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": >int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } }, "type": "track" } ] } """ self._check_scope( "get_album_items", "r_usr", flows={"device_code"}, require_authentication=False, ) url = f"{self.API_URL}/v1/albums/{album_id}/items" if credits: url += "/credits" return self._get_json( url, params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_album_credits( self, album_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get credits for an album. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_id : `int` or `str` TIDAL album ID. **Example**: :code:`251380836`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- credits : `dict` A dictionary containing TIDAL catalog information for the album contributors. .. admonition:: Sample response :class: dropdown .. code:: [ { "type": <str>, "contributors": [ { "name": <str> } ] } ] """ self._check_scope( "get_album_credits", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/albums/{album_id}/credits", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_album_review( self, album_id: Union[int, str], country_code: str = None ) -> dict[str, str]: """ Get a review of or a synopsis for an album. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_id : `int` or `str` TIDAL album ID. **Example**: :code:`251380836`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- review : `dict` A dictionary containing a review of or a synopsis for an album and its source. .. admonition:: Sample response :class: dropdown .. code:: { "source": <str>, "lastUpdated": <str>, "text": <str>, "summary": <str> } """ self._check_scope( "get_album_review", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/albums/{album_id}/review", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_similar_albums( self, album_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for albums similar to the specified album. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_id : `int` or `str` TIDAL album ID. **Example**: :code:`251380836`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- album : `dict` TIDAL catalog information for an album. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "premiumStreamingOnly": <bool>, "numberOfTracks": <int>, "numberOfVideos": <int>, "numberOfVolumes": <int>, "releaseDate": <str>, "copyright": <str>, "type": "ALBUM", "version": <str>, "url": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str>, "explicit": <bool>, "upc": <str>, "popularity": <int>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ] } ], "source": <str> } """ self._check_scope( "get_similar_albums", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/albums/{album_id}/similar", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_favorite_albums( self, country_code: str = None, *, limit: int = 50, offset: int = None, order: str = "DATE", order_direction: str = "DESC", ) -> dict[str, Any]: """ Get TIDAL catalog information for albums in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. order : `str`, keyword-only, default: :code:`"DATE"` Sorting order. **Valid values**: :code:`"DATE"` and :code:`"NAME"`. order_direction : `str`, keyword-only, default: :code:`"DESC"` Sorting order direction. **Valid values**: :code:`"DESC"` and :code:`"ASC"`. Returns ------- albums : `dict` A dictionary containing TIDAL catalog information for albums in the current user's collection and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "created": <str>, "item": { "id": <int>, "title": <str>, "duration": <int>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "premiumStreamingOnly": <bool>, "numberOfTracks": <int>, "numberOfVideos": <int>, "numberOfVolumes": <int>, "releaseDate": <str>, "copyright": <str>, "type": "ALBUM", "version": <str>, "url": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str>, "explicit": <bool>, "upc": <str>, "popularity": <int>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ] } } ] } """ self._check_scope( "get_favorite_albums", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/favorites/albums", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, "order": order, "orderDirection": order_direction, }, )
[docs] def favorite_albums( self, album_ids: Union[int, str, list[Union[int, str]]], country_code: str = None, *, on_artifact_not_found: str = "FAIL", ) -> None: """ Add albums to the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_ids : `int`, `str`, or `list` TIDAL album ID(s). **Examples**: :code:`"251380836,275646830"` or :code:`[251380836, 275646830]`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added does not exist. **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`. """ self._check_scope("favorite_albums", "r_usr", flows={"device_code"}) self._request( "post", f"{self.API_URL}/v1/users/{self._user_id}/favorites/albums", params={"countryCode": self._get_country_code(country_code)}, data={ "albumIds": ( ",".join(map(str, album_ids)) if isinstance(album_ids, list) else album_ids ), "onArtifactNotFound": on_artifact_not_found, }, )
[docs] def unfavorite_albums( self, album_ids: Union[int, str, list[Union[int, str]]] ) -> None: """ Remove albums from the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_ids : `int`, `str`, or `list` TIDAL album ID(s). **Examples**: :code:`"251380836,275646830"` or :code:`[251380836, 275646830]`. """ self._check_scope("unfavorite_albums", "r_usr", flows={"device_code"}) if isinstance(album_ids, list): album_ids = ",".join(map(str, album_ids)) self._request( "delete", f"{self.API_URL}/v1/users/{self._user_id}" f"/favorites/albums/{album_ids}", )
### ARTISTS ###############################################################
[docs] def get_artist( self, artist_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for an artist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- artist : `dict` TIDAL catalog information for an artist. .. admonition:: Sample response :class: dropdown .. code:: { "id": <int>, "name": <str>, "artistTypes": [<str>], "url": <str>, "picture": <str>, "popularity": <int>, "artistRoles": [ { "categoryId": <int>, "category": <str> } ], "mixes": { "ARTIST_MIX": <str> } } """ self._check_scope( "get_artist", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_artist_albums( self, artist_id: Union[int, str], country_code: str = None, *, filter: str = None, limit: int = 100, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for albums by an artist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. filter : `str`, keyword-only, optional Subset of albums to retrieve. **Valid values**: :code:`"EPSANDSINGLES"` and :code:`"COMPILATIONS"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- albums : `dict` A dictionary containing TIDAL catalog information for albums by the specified artist and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "premiumStreamingOnly": <bool>, "numberOfTracks": <int>, "numberOfVideos": <int>, "numberOfVolumes": <int>, "releaseDate": <str>, "copyright": <str>, "type": "ALBUM", "version": <str>, "url": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str>, "explicit": <bool>, "upc": <str>, "popularity": <int>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ] } ] } """ self._check_scope( "get_artist_albums", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/albums", params={ "countryCode": self._get_country_code(country_code), "filter": filter, "limit": limit, "offset": offset, }, )
[docs] def get_artist_top_tracks( self, artist_id: Union[int, str], country_code: str = None, *, limit: int = 100, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for an artist's top tracks. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- tracks : `dict` A dictionary containing TIDAL catalog information for the artist's top tracks and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": <int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } } ] } """ self._check_scope( "get_artist_top_tracks", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/toptracks", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_artist_videos( self, artist_id: Union[int, str], country_code: str = None, *, limit: int = 100, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for an artist's videos. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- videos : `dict` A dictionary containing TIDAL catalog information for the artist's videos and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "volumeNumber": <int>, "trackNumber": <int>, "releaseDate": <str>, "imagePath": <str>, "imageId": <str>, "vibrantColor": <str>, "duration": <int>, "quality": <str>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "explicit": <bool>, "popularity": <int>, "type": "Music Video", "adsUrl": <str>, "adsPrePaywallOnly": <bool>, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": <dict> } ] } """ self._check_scope( "get_artist_videos", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/videos", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_artist_mix_id( self, artist_id: Union[int, str], country_code: str = None ) -> str: """ Get the ID of a curated mix of tracks based on an artist's works. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- mix_id : `str` TIDAL mix ID. **Example**: :code:`"000ec0b01da1ddd752ec5dee553d48"`. """ self._check_scope( "get_artist_mix_id", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/mix", params={"countryCode": self._get_country_code(country_code)}, )["id"]
[docs] def get_artist_radio( self, artist_id: Union[int, str], country_code: str = None, *, limit: int = None, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for tracks inspired by an artist's works. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. .. note:: This method is functionally identical to first getting the artist mix ID using :meth:`get_artist_mix_id` and then retrieving TIDAL catalog information for the items in the mix using :meth:`get_mix_items`. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, optional Page size. **Default**: :code:`100`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- tracks : `dict` A dictionary containing TIDAL catalog information for tracks inspired by an artist's works and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": <int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } } ] } """ self._check_scope( "get_artist_radio", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/radio", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_artist_biography( self, artist_id: Union[int, str], country_code: str = None ) -> dict[str, str]: """ Get an artist's biographical information. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- biography : `dict` A dictionary containing an artist's biographical information and its source. .. admonition:: Sample response :class: dropdown .. code:: { "source": <str>, "lastUpdated": <str>, "text": <str>, "summary": <str> } """ self._check_scope( "get_artist_biography", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/bio", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_similar_artists( self, artist_id: str, country_code: str = None, *, limit: int = None, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for artists similar to a specified artist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- artists : `dict` A dictionary containing TIDAL catalog information for artists similar to the specified artist and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "name": <str>, "type": None, "artistTypes": [<str>], "url": <str>, "picture": <str>, "popularity": <int>, "banner": <str>, "artistRoles": <list>, "mixes": <dict>, "relationType": "SIMILAR_ARTIST" } ], "source": "TIDAL" } """ self._check_scope( "get_similar_artists", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/artists/{artist_id}/similar", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_favorite_artists( self, country_code: str = None, *, limit: int = 50, offset: int = None, order: str = "DATE", order_direction: str = "DESC", ) -> dict[str, Any]: """ Get TIDAL catalog information for artists in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. order : `str`, keyword-only, default: :code:`"DATE"` Sorting order. **Valid values**: :code:`"DATE"` and :code:`"NAME"`. order_direction : `str`, keyword-only, default: :code:`"DESC"` Sorting order direction. **Valid values**: :code:`"DESC"` and :code:`"ASC"`. Returns ------- artists : `dict` A dictionary containing TIDAL catalog information for artists in the current user's collection and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "created": <str>, "item": { "id": <int>, "name": <str>, "artistTypes": [<str>], "url": <str>, "picture": <str>, "popularity": <int>, "artistRoles": [ { "categoryId": <int>, "category": <str> } ], "mixes": { "ARTIST_MIX": <str> } } } ] } """ self._check_scope( "get_favorite_artists", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/favorites/artists", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, "order": order, "orderDirection": order_direction, }, )
[docs] def favorite_artists( self, artist_ids: Union[int, str, list[Union[int, str]]], country_code: str = None, *, on_artifact_not_found: str = "FAIL", ) -> None: """ Add artists to the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_ids : `int`, `str`, or `list` TIDAL artist ID(s). **Examples**: :code:`"1566,7804"` or :code:`[1566, 7804]`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added does not exist. **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`. """ self._check_scope("favorite_artists", "r_usr", flows={"device_code"}) self._request( "post", f"{self.API_URL}/v1/users/{self._user_id}/favorites/artists", params={"countryCode": self._get_country_code(country_code)}, data={ "artistIds": ( ",".join(map(str, artist_ids)) if isinstance(artist_ids, list) else artist_ids ), "onArtifactNotFound": on_artifact_not_found, }, )
[docs] def unfavorite_artists( self, artist_ids: Union[int, str, list[Union[int, str]]] ) -> None: """ Remove artists from the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_ids : `int`, `str`, or `list` TIDAL artist ID(s). **Examples**: :code:`"1566,7804"` or :code:`[1566, 7804]`. """ self._check_scope("unfavorite_artists", "r_usr", flows={"device_code"}) if isinstance(artist_ids, list): artist_ids = ",".join(map(str, artist_ids)) self._request( "delete", f"{self.API_URL}/v1/users/{self._user_id}" f"/favorites/artists/{artist_ids}", )
[docs] def get_blocked_artists( self, *, limit: int = 50, offset: int = None ) -> dict[str, Any]: """ Get TIDAL catalog information for the current user's blocked artists. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- artists : `dict` A dictionary containing TIDAL catalog information for the the current user's blocked artists and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "item": { "id": <int>, "name": <str>, "type": <str>, "artistTypes": [<str>], "url": <str>, "picture": <str>, "popularity": <int>, "banner": <str>, "artistRoles": [ { "categoryId": <int>, "category": <str> } ], "mixes": { "ARTIST_MIX": <str> } }, "created": <str>, "type": "ARTIST" } ] } """ self._check_scope( "get_blocked_artists", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/blocks/artists", params={"limit": limit, "offset": offset}, )
[docs] def block_artist(self, artist_id: Union[int, str]) -> None: """ Block an artist from appearing in mixes and the radio. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. """ self._check_scope("block_artist", "r_usr", flows={"device_code"}) self._request( "post", f"{self.API_URL}/v1/users/{self._user_id}/blocks/artists", data={"artistId": artist_id}, )
[docs] def unblock_artist(self, artist_id: Union[int, str]) -> None: """ Unblock an artist from appearing in mixes and the radio. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. """ self._check_scope("unblock_artist", "r_usr", flows={"device_code"}) self._request( "delete", f"{self.API_URL}/v1/users/{self._user_id}" f"/blocks/artists/{artist_id}", )
### COUNTRY ###############################################################
[docs] def get_country_code(self) -> str: """ Get the country code based on the current IP address. Returns ------- country : `str`, keyword-only, optional ISO 3166-1 alpha-2 country code. **Example**: :code:`"US"`. """ return self._get_json(f"{self.API_URL}/v1/country")["countryCode"]
### IMAGES ################################################################
[docs] def get_image( self, uuid: str, animated: bool = False, *, width: int = None, height: int = None, filename: Union[str, pathlib.Path] = None, ) -> bytes: """ Get (animated) cover art or image for a TIDAL item. .. note:: This method is provided for convenience and is not a private TIDAL API endpoint. Parameters ---------- uuid : `str` Image UUID. **Example**: :code:`"d3c4372b-a652-40e0-bdb1-fc8d032708f6"`. animated : `bool`, default: :code:`False` Specifies whether the image is animated. width : `int`, keyword-only, optional Valid image width for the item type. If `width` or `height` is not specified, the original dimensions of the image is used. height : `int`, keyword-only, optional Valid image height for the item type. If `width` or `height` is not specified, the original dimensions of the image is used. filename : `str` or `pathlib.Path`, keyword-only, optional Filename with the :code:`.jpg` or :code:`.mp4` extension. If specified, the image is saved to a file instead. Returns ------- image : `bytes` Image data. If :code:`save=True`, the stream data is saved to an image or video file and its filename is returned instead. """ if width is None or height is None: dimensions = "origin" else: dimensions = f"{width}x{height}" if animated: extension = ".mp4" media_type = "videos" else: extension = ".jpg" media_type = "images" with self.session.get( f"{self.RESOURCES_URL}/{media_type}" f"/{uuid.replace('-', '/')}" f"/{dimensions}{extension}" ) as r: image = r.content if filename: if not isinstance(filename, pathlib.Path): filename = pathlib.Path(filename) if not filename.name.endswith(extension): filename += extension with open(filename, "wb") as f: f.write(image) else: return image
### MIXES #################################################################
[docs] def get_mix_items( self, mix_id: str, country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for items (tracks and videos) in a mix. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- mix_id : `str` TIDAL mix ID. **Example**: :code:`"000ec0b01da1ddd752ec5dee553d48"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- items : `dict` A dictionary containing TIDAL catalog information for tracks and videos in the specified mix and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "item": { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": >int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } }, "type": "track" } ] } """ self._check_scope( "get_mix_items", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/mixes/{mix_id}/items", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_favorite_mixes( self, *, ids: bool = False, limit: int = 50, cursor: str = None, order: str = "DATE", order_direction: str = "DESC", ) -> dict[str, Any]: """ Get TIDAL catalog information for or IDs of mixes in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- ids : `bool`, keyword-only, default: :code:`False` Determine whether TIDAL catalog information about the mixes (:code:`False`) or the mix IDs (:code:`True`) are returned. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. cursor : `str`, keyword-only, optional Cursor position of the last item in previous search results. Use with `limit` to get the next page of search results. order : `str`, keyword-only, default: :code:`"DATE"` Sorting order. **Valid values**: :code:`"DATE"` and :code:`"NAME"`. order_direction : `str`, keyword-only, default: :code:`"DESC"` Sorting order direction. **Valid values**: :code:`"DESC"` and :code:`"ASC"`. Returns ------- mixes : `dict` A dictionary containing the TIDAL catalog information for or IDs of the mixes in the current user's collection and the cursor position. .. admonition:: Sample response :class: dropdown .. code:: { "items": [ { "dateAdded": <str>, "title": <str>, "id": <str>, "mixType": <str>, "updated": <str>, "subTitleTextInfo": { "text": <str>, "color": <str> }, "images": { "SMALL": { "width": <int>, "height": <int>, "url": <str> }, "MEDIUM": { "width": <int>, "height": <int>, "url": <str> }, "LARGE": { "width": <int>, "height": <int>, "url": <str> }, }, "detailImages": { "SMALL": { "width": <int>, "height": <int>, "url": <str> }, "MEDIUM": { "width": <int>, "height": <int>, "url": <str> }, "LARGE": { "width": <int>, "height": <int>, "url": <str> } }, "master": <bool>, "subTitle": <str>, "titleTextInfo": { "text": <str>, "color": <str> } } ], "cursor": <str>, "lastModifiedAt": <str> } """ self._check_scope("get_favorite_mixes", "r_usr", flows={"device_code"}) url = f"{self.API_URL}/v2/favorites/mixes" if ids: url += "/ids" return self._get_json( url, params={ "limit": limit, "cursor": cursor, "order": order, "orderDirection": order_direction, }, )
[docs] def favorite_mixes( self, mix_ids: Union[str, list[str]], *, on_artifact_not_found: str = "FAIL", ) -> None: """ Add mixes to the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- mix_ids : `str` or `list` TIDAL mix ID(s). **Examples**: :code:`"000ec0b01da1ddd752ec5dee553d48,\ 000dd748ceabd5508947c6a5d3880a"` or :code:`["000ec0b01da1ddd752ec5dee553d48", "000dd748ceabd5508947c6a5d3880a"]` on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added does not exist. **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`. """ self._check_scope("favorite_mixes", "r_usr", flows={"device_code"}) self._request( "put", f"{self.API_URL}/v2/favorites/mixes/add", data={ "mixIds": mix_ids, "onArtifactNotFound": on_artifact_not_found, }, )
[docs] def unfavorite_mixes(self, mix_ids: Union[str, list[str]]) -> None: """ Remove mixes from the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- mix_ids : `str` or `list` TIDAL mix ID(s). **Examples**: :code:`"000ec0b01da1ddd752ec5dee553d48,\ 000dd748ceabd5508947c6a5d3880a"` or :code:`["000ec0b01da1ddd752ec5dee553d48", "000dd748ceabd5508947c6a5d3880a"]` """ self._check_scope("unfavorite_mixes", "r_usr", flows={"device_code"}) self._request( "put", f"{self.API_URL}/v2/favorites/mixes/remove", data={"mixIds": mix_ids}, )
### PAGES #################################################################
[docs] def get_album_page( self, album_id: Union[int, str], country_code: str = None, *, device_type: str = "BROWSER", ) -> dict[str, Any]: """ Get the TIDAL page for an album. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- album_id : `int` or `str` TIDAL album ID. **Example**: :code:`251380836`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. device_type : `str`, keyword-only, default: :code:`"BROWSER"` Device type. .. container:: **Valid values**: * :code:`"BROWSER"` for a web browser. * :code:`"DESKTOP"` for the desktop TIDAL application. * :code:`"PHONE"` for the mobile TIDAL application. * :code:`"TV"` for the smart TV TIDAL application. Returns ------- page : `dict` A dictionary containing the page ID, title, and submodules. """ self._check_scope( "get_album_page", "r_usr", flows={"device_code"}, require_authentication=False, ) if device_type not in ( DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"} ): emsg = f"Invalid device type. Valid values: {', '.join(DEVICE_TYPES)}." raise ValueError(emsg) return self._get_json( f"{self.API_URL}/v1/pages/album", params={ "albumId": album_id, "countryCode": self._get_country_code(country_code), "deviceType": device_type, }, )
[docs] def get_artist_page( self, artist_id: Union[int, str], country_code: str = None, *, device_type: str = "BROWSER", ) -> dict[str, Any]: """ Get the TIDAL page for an artist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- artist_id : `int` or `str` TIDAL artist ID. **Example**: :code:`1566`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. device_type : `str`, keyword-only, default: :code:`"BROWSER"` Device type. .. container:: **Valid values**: * :code:`"BROWSER"` for a web browser. * :code:`"DESKTOP"` for the desktop TIDAL application. * :code:`"PHONE"` for the mobile TIDAL application. * :code:`"TV"` for the smart TV TIDAL application. Returns ------- page : `dict` A dictionary containing the page ID, title, and submodules. """ self._check_scope( "get_artist_page", "r_usr", flows={"device_code"}, require_authentication=False, ) if device_type not in ( DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"} ): emsg = f"Invalid device type. Valid values: {', '.join(DEVICE_TYPES)}." raise ValueError(emsg) return self._get_json( f"{self.API_URL}/v1/pages/artist", params={ "artistID": artist_id, "countryCode": self._get_country_code(country_code), "deviceType": device_type, }, )
[docs] def get_mix_page( self, mix_id: str, country_code: str = None, *, device_type: str = "BROWSER", ) -> dict[str, Any]: """ Get the TIDAL page for a mix. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- mix_id : `str` TIDAL mix ID. **Example**: :code:`"000ec0b01da1ddd752ec5dee553d48"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. device_type : `str`, keyword-only, default: :code:`"BROWSER"` Device type. .. container:: **Valid values**: * :code:`"BROWSER"` for a web browser. * :code:`"DESKTOP"` for the desktop TIDAL application. * :code:`"PHONE"` for the mobile TIDAL application. * :code:`"TV"` for the smart TV TIDAL application. Returns ------- page : `dict` A dictionary containing the page ID, title, and submodules. """ self._check_scope( "get_mix_page", "r_usr", flows={"device_code"}, require_authentication=False, ) if device_type not in ( DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"} ): emsg = f"Invalid device type. Valid values: {', '.join(DEVICE_TYPES)}." raise ValueError(emsg) return self._get_json( f"{self.API_URL}/v1/pages/mix", params={ "mixId": mix_id, "countryCode": self._get_country_code(country_code), "deviceType": device_type, }, )
[docs] def get_video_page( self, video_id: Union[int, str], country_code: str = None, *, device_type: str = "BROWSER", ) -> dict[str, Any]: """ Get the TIDAL page for a video. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- video_id : `int` or `str` TIDAL video ID. **Example**: :code:`75623239`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. device_type : `str`, keyword-only, default: :code:`"BROWSER"` Device type. .. container:: **Valid values**: * :code:`"BROWSER"` for a web browser. * :code:`"DESKTOP"` for the desktop TIDAL application. * :code:`"PHONE"` for the mobile TIDAL application. * :code:`"TV"` for the smart TV TIDAL application. Returns ------- page : `dict` A dictionary containing the page ID, title, and submodules. """ self._check_scope( "get_video_page", "r_usr", flows={"device_code"}, require_authentication=False, ) if device_type not in ( DEVICE_TYPES := {"BROWSER", "DESKTOP", "PHONE", "TV"} ): emsg = f"Invalid device type. Valid values: {', '.join(DEVICE_TYPES)}." raise ValueError(emsg) return self._get_json( f"{self.API_URL}/v1/pages/videos", params={ "videoId": video_id, "countryCode": self._get_country_code(country_code), "deviceType": device_type, }, )
### PLAYLISTS #############################################################
[docs] def get_playlist( self, playlist_uuid: str, country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for a playlist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- playlist : `dict` TIDAL catalog information for a playlist. .. admonition:: Sample response :class: dropdown .. code:: { "uuid": <str>, "title": <str>, "numberOfTracks": <int>, "numberOfVideos": <int>, "creator": { "id": <int> }, "description": <str>, "duration": <int>, "lastUpdated": <str>, "created": <str>, "type": <str>, "publicPlaylist": <bool>, "url": <str>, "image": <str>, "popularity": <int>, "squareImage": <str>, "promotedArtists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str>, } ], "lastItemAddedAt": <str> } """ self._check_scope( "get_playlist", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/playlists/{playlist_uuid}", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_playlist_etag( self, playlist_uuid: str, country_code: str = None ) -> str: """ Get the entity tag (ETag) for a playlist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. .. note:: This method is provided for convenience and is not a private TIDAL API endpoint. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- etag : `str` ETag for a playlist. **Example**: :code:`"1698984074453"`. """ self._check_scope( "get_playlist_etag", "r_usr", flows={"device_code"}, require_authentication=False, ) r = self._request( "get", f"{self.API_URL}/v1/playlists/{playlist_uuid}", params={"countryCode": self._get_country_code(country_code)}, ) return r.headers["ETag"].replace('"', "")
[docs] def get_playlist_items( self, playlist_uuid: str, country_code: str = None, *, limit: int = 100, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for items (tracks and videos) in a playlist. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- items : `dict` A dictionary containing TIDAL catalog information for tracks and videos in the specified playlist and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "item": { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": >int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } }, "type": "track" } ] } """ self._check_scope( "get_playlist_items", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/playlists/{playlist_uuid}/items", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_playlist_recommendations( self, playlist_uuid: str, country_code: str = None, *, limit: int = None, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for recommended tracks based on a playlist's items. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, optional Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- items : `dict` A dictionary containing TIDAL catalog information for recommended tracks and videos and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "item": { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": >int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } }, "type": "track" } ] } """ self._check_scope( "get_playlist_recommendations", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/playlists/{playlist_uuid}" "/recommendations/items", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def favorite_playlists( self, playlist_uuids: Union[str, list[str]], *, folder_id: str = "root" ) -> None: """ Add playlists to the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuids : `str` or `list` TIDAL playlist UUID(s). **Example**: :code:`["36ea71a8-445e-41a4-82ab-6628c581535d", "4261748a-4287-4758-aaab-6d5be3e99e52"]`. folder_id : `str`, keyword-only, default: :code:`"root"` ID of the folder to move the playlist into. To place a playlist directly under "My Playlists", use :code:`folder_id="root"`. """ self._check_scope("favorite_playlists", "r_usr", flow={"device_code"}) self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/add-favorites", params={"uuids": playlist_uuids, "folderId": folder_id}, )
[docs] def move_playlist(self, playlist_uuid: str, folder_id: str) -> None: """ Move a playlist in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`. folder_id : `str` ID of the folder to move the playlist into. To place a playlist directly under "My Playlists", use :code:`folder_id="root"`. """ self._check_scope("move_playlist", "r_usr", flows={"device_code"}) self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/move", params={ "folderId": folder_id, "trns": f"trn:playlist:{playlist_uuid}", }, )
[docs] def unfavorite_playlist(self, playlist_uuid: str) -> None: """ Remove a playlist from the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`. """ self._check_scope( "unfavorite_playlist", "r_usr", flows={"device_code"} ) self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/remove", params={"trns": f"trn:playlist:{playlist_uuid}"}, )
[docs] def get_user_playlist(self, playlist_uuid: str) -> dict[str, Any]: """ Get TIDAL catalog information for a user playlist. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL user playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. Returns ------- playlist : `dict` TIDAL catalog information for a user playlist. .. admonition:: Sample response :class: dropdown .. code:: { "playlist": { "uuid": <str>, "type": "USER", "creator": { "id": <int>, "name": <str>, "picture": <str>, "type": "USER" }, "contentBehavior": <str>, "sharingLevel": <str>, "status": <str>, "source": <str>, "title": <str>, "description": <str>, "image": <str>, "squareImage": <str>, "url": <str>, "created": <str>, "lastUpdated": <str>, "lastItemAddedAt": <str>, "duration": <int>, "numberOfTracks": <int>, "numberOfVideos": <int>, "promotedArtists": [], "trn": <str>, }, "followInfo": { "nrOfFollowers": <int>, "tidalResourceName": <str>, "followed": <bool>, "followType": "PLAYLIST" }, "profile": { "userId": <int>, "name": <str>, "color": [<str>] } } """ self._check_scope("get_user_playlist", "r_usr", flows={"device_code"}) return self._get_json( f"{self.API_URL}/v2/user-playlists/{playlist_uuid}" )
[docs] def get_user_playlists( self, user_id: Union[int, str] = None, *, limit: int = 50, cursor: str = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for playlists created by a TIDAL user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `str` TIDAL user ID. If not specified, the ID associated with the user account in the current session is used. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. cursor : `str`, keyword-only, optional Cursor position of the last item in previous search results. Use with `limit` to get the next page of search results. Returns ------- playlists : `dict` A dictionary containing the user's playlists and the cursor position. .. admonition:: Sample response :class: dropdown .. code:: { "items": [ { "playlist": { "uuid": <str>, "type": "USER", "creator": { "id": <int>, "name": <str>, "picture": <str>, "type": "USER" }, "contentBehavior": <str>, "sharingLevel": <str>, "status": <str>, "source": <str>, "title": <str>, "description": <str>, "image": <str>, "squareImage": <str>, "url": <str>, "created": <str>, "lastUpdated": <str>, "lastItemAddedAt": <str>, "duration": <int>, "numberOfTracks": <int>, "numberOfVideos": <int>, "promotedArtists": [], "trn": <str>, }, "followInfo": { "nrOfFollowers": <int>, "tidalResourceName": <str>, "followed": <bool>, "followType": "PLAYLIST" }, "profile": { "userId": <int>, "name": <str>, "color": [<str>] } } ], "cursor": <str> } """ self._check_scope("get_user_playlists", "r_usr", flows={"device_code"}) if user_id is None: user_id = self._user_id return self._get_json( f"{self.API_URL}/v2/user-playlists/{user_id}/public", params={"limit": limit, "cursor": cursor}, )
[docs] def get_personal_playlists( self, country_code: str = None, *, limit: int = 50, offset: int = None ) -> dict[str, Any]: """ Get TIDAL catalog information for playlists created by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- playlists : `dict` TIDAL catalog information for a user playlists created by the current user and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "uuid": <str>", "title": <str>, "numberOfTracks": <int>, "numberOfVideos": <int>, "creator": { "id": <int> }, "description": <str>, "duration": <int>, "lastUpdated": <str>, "created": <str>, "type": "USER", "publicPlaylist": <bool>, "url": <str>, "image": <str>, "popularity": <int>, "squareImage": <str>, "promotedArtists": [], "lastItemAddedAt": <str> } ] } """ self._check_scope( "get_personal_playlists", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/playlists", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def create_playlist( self, name: str, *, description: str = None, folder_uuid: str = "root", public: bool = None, ) -> dict[str, Any]: """ Create a user playlist. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- name : `str` Playlist name. description : `str`, keyword-only, optional Brief playlist description. folder_uuid : `str`, keyword-only, default: :code:`"root"` UUID of the folder the new playlist will be placed in. To place a playlist directly under "My Playlists", use :code:`folder_id="root"`. public : `bool`, keyword-only, optional Determines whether the playlist is public (:code:`True`) or private (:code:`False`). Returns ------- playlist : `dict` TIDAL catalog information for the newly created playlist. .. admonition:: Sample response :class: dropdown .. code:: { "trn": <str>, "itemType": "PLAYLIST", "addedAt": <str>, "lastModifiedAt": <str>, "name": <str>, "parent": <str>, "data": { "uuid": <str>, "type": "USER", "creator": { "id": <int>, "name": <str>, "picture": <str>, "type": "USER" }, "contentBehavior": <str>, "sharingLevel": <str>, "status": "READY", "source": <str>, "title": <str>, "description": <str>, "image": <str>, "squareImage": <str>, "url": <str>, "created": <str>, "lastUpdated": <str>, "lastItemAddedAt": <str>, "duration": <int>, "numberOfTracks": <int>, "numberOfVideos": <int>, "promotedArtists": <list>, "trn": <str>, "itemType": "PLAYLIST" } } """ self._check_scope("create_playlist", "r_usr", flows={"device_code"}) return self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/create-playlist", params={ "name": name, "description": description, "folderId": folder_uuid, "isPublic": public, }, ).json()
[docs] def update_playlist( self, playlist_uuid: str, *, title: str = None, description: str = None ) -> None: """ Update the title or description of a playlist owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. title : `str`, keyword-only, optional New playlist title. description : `str`, keyword-only, optional New playlist description. """ self._check_scope("update_playlist", "r_usr", flows={"device_code"}) if title is None and description is None: wmsg = "No changes were specified or made to the playlist." warnings.warn(wmsg) return data = {} if title is not None: data["title"] = title if description is not None: data["description"] = description self._request( "post", f"{self.API_URL}/v1/playlists/{playlist_uuid}", data=data )
[docs] def set_playlist_privacy(self, playlist_uuid: str, public: bool) -> None: """ Set the privacy of a playlist owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. public : `bool` Determines whether the playlist is public (:code:`True`) or private (:code:`False`). """ self._check_scope( "set_playlist_privacy", "r_usr", flows={"device_code"} ) self._request( "put", f"{self.API_URL}/v2/playlists/{playlist_uuid}/set-" f"{'public' if public else 'private'}", )
[docs] def add_playlist_items( self, playlist_uuid: str, items: Union[int, str, list[Union[int, str]]] = None, *, from_album_id: str = None, from_playlist_uuid: str = None, on_duplicate: str = "FAIL", on_artifact_not_found: str = "FAIL", ) -> None: """ Add items to a playlist owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. items : `int`, `str`, or `list`, optional Items to add to the playlist. If not specified, `from_playlist_uuid` must be provided. .. note:: If both `items` and `from_playlist_uuid` are specified, only the items in `items` will be added to the playlist. from_playlist_uuid : `str`, keyword-only, optional TIDAL playlist from which to copy items. on_duplicate : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added is already in the playlist. **Valid values**: :code:`"ADD"`, :code:`"SKIP"`, and :code:`"FAIL"`. on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added does not exist. **Valid values**: :code:`"FAIL"`. """ self._check_scope("add_playlist_items", "r_usr", flows={"device_code"}) if items is None and from_playlist_uuid is None: wmsg = "No changes were specified or made to the playlist." warnings.warn(wmsg) return data = { "onArtifactNotFound": on_artifact_not_found, "onDupes": on_duplicate, } if items: data |= {"trackIds": items} elif from_album_id: data |= {"fromAlbumId": from_album_id} else: data |= {"fromPlaylistUuid": from_playlist_uuid} self._request( "post", f"{self.API_URL}/v1/playlists/{playlist_uuid}/items", data=data, headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}, )
[docs] def move_playlist_items( self, playlist_uuid: str, from_indices: Union[int, str], to_index: Union[int, str], ) -> None: """ Move an item in a playlist owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. from_indices : `int` or `str` Current item indices, provided as an integer or a comma-separated string. to_index : `int` or `str` Desired item index. """ self._check_scope("move_playlist_item", "r_usr", flows={"device_code"}) self._request( "post", f"{self.API_URL}/v1/playlists/{playlist_uuid}/items/{from_indices}", params={"toIndex": to_index}, headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}, )
[docs] def replace_playlist_item( self, playlist_uuid: str, index: Union[int, str], item: Union[int, str] ) -> None: """ Replace an item in a playlist owned by the current user with another item. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. index : int or str; positional-only Zero-based index of the item to be replaced. **Examples**: :code:`1`, :code:`"2"`. item : int or str; positional-only TIDAL ID of the track or video to replace the item at index `index`. **Examples**: :code:`46369325`, :code:`"75413016"`. """ self._check_scope( "replace_playlist_item", "r_usr", flows={"device_code"} ) self._request( "post", f"{self.API_URL}/v1/playlists/{playlist_uuid}/items/{index}", data={"itemId": item}, headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}, )
[docs] def delete_playlist_items( self, playlist_uuid: str, indices: Union[int, str] ) -> None: """ Delete items from a playlist owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. indices : `int` or `str` Item indices, provided as an integer or a comma-separated string. """ self._check_scope( "delete_playlist_item", "r_usr", flows={"device_code"} ) self._request( "delete", f"{self.API_URL}/v1/playlists/{playlist_uuid}/items/{indices}", headers={"If-None-Match": self.get_playlist_etag(playlist_uuid)}, )
[docs] def delete_playlist(self, playlist_uuid: str) -> None: """ Delete a playlist owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- playlist_uuid : `str` TIDAL playlist UUID. **Example**: :code:`"e09ab9ce-2e87-41b8-b404-3cd712bf706e"`. """ self._check_scope("delete_playlist", "r_usr", flows={"device_code"}) self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/remove", params={"trns": f"trn:playlist:{playlist_uuid}"}, )
[docs] def get_personal_playlist_folders( self, folder_uuid: str = None, *, flattened: bool = False, include_only: str = None, limit: int = 50, order: str = "DATE", order_direction: str = "DESC", ) -> dict[str, Any]: """ Get TIDAL catalog information for a playlist folder (and optionally, playlists and other playlist folders in it) created by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- folder_uuid : `str`, optional UUID of the folder in which to look for playlists and other folders. If not specified, all folders and playlists in "My Playlists" are returned. flattened : `bool`, keyword-only, default: :code:`False` Determines whether the results are flattened into a list. include_only : `str`, keyword-only, optional Type of playlist-related item to return. **Valid values**: :code:`"FAVORITE_PLAYLIST"`, :code:`"FOLDER"`, and :code:`"PLAYLIST"`. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. order : `str`, keyword-only, default: :code:`"DATE"` Sorting order. **Valid values**: :code:`"DATE"`, :code:`"DATE_UPDATED"`, and :code:`"NAME"`. order_direction : `str`, keyword-only, default: :code:`"DESC"` Sorting order direction. **Valid values**: :code:`"DESC"` and :code:`"ASC"`. Returns ------- items : `dict` A dictionary containing playlist-related items and the total number of items available. .. admonition:: Sample response :class: dropdown .. code:: { "lastModifiedAt": <str>, "items": [ { "trn": <str>, "itemType": "FOLDER", "addedAt": <str>, "lastModifiedAt": <str>, "name": <str>, "parent": <str>, "data": { "trn": <str>, "name": <str>, "createdAt": <str>, "lastModifiedAt": <str>, "totalNumberOfItems": <int>, "id": <str>, "itemType": "FOLDER" } } ], "totalNumberOfItems": <int>, "cursor": <str> } """ self._check_scope( "get_personal_playlist_folders", "r_usr", flows={"device_code"} ) if include_only and include_only not in ( ALLOWED_INCLUDES := {"FAVORITE_PLAYLIST", "FOLDER", "PLAYLIST"} ): emsg = ( "Invalid include type. Valid values: " f"{', '.join(ALLOWED_INCLUDES)}." ) raise ValueError(emsg) url = f"{self.API_URL}/v2/my-collection/playlists/folders" if flattened: url += "/flattened" return self._get_json( url, params={ "folderId": folder_uuid if folder_uuid else "root", "limit": limit, "includeOnly": include_only, "order": order, "orderDirection": order_direction, }, )
[docs] def create_playlist_folder( self, name: str, *, folder_uuid: str = "root" ) -> dict[str, Any]: """ Create a user playlist folder. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- name : `str` Playlist folder name. folder_uuid : `str`, keyword-only, default: :code:`"root"` UUID of the folder in which the new playlist folder should be created in. To create a folder directly under "My Playlists", use :code:`folder_id="root"`. """ self._check_scope( "create_playlist_folder", "r_usr", flows={"device_code"} ) return self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/create-folder", params={"name": name, "folderId": folder_uuid}, )
[docs] def delete_playlist_folder(self, folder_uuid: str) -> None: """ Delete a playlist folder owned by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- folder_uuid : `str` TIDAL playlist folder UUID. **Example**: :code:`"92b3c1ea-245a-4e5a-a5a4-c215f7a65b9f"`. """ self._check_scope( "delete_playlist_folder", "r_usr", flows={"device_code"} ) self._request( "put", f"{self.API_URL}/v2/my-collection/playlists/folders/remove", params={"trns": f"trn:folder:{folder_uuid}"}, )
### SEARCH ################################################################
[docs] def search( self, query: str, country_code: str = None, *, type: str = None, limit: int = None, offset: int = None, ) -> dict[str, Any]: """ Search for albums, artists, tracks, and videos. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- query : `str` Search query. **Example**: :code:`"Beyoncé"`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. type : `str`, keyword-only, optional Target search type. Searches for all types if not specified. **Valid values**: :code:`"ALBUMS"`, :code:`"ARTISTS"`, :code:`"TRACKS"`, :code:`"VIDEOS"`. limit : `int`, keyword-only, optional Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- results : `dict` A dictionary containing TIDAL catalog information for albums, artists, tracks, and videos matching the search query, and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "artists": { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "name": <str>, "artistTypes": [<str>], "url": <str>, "picture": <str>, "popularity": <int>, "artistRoles": [ { "categoryId": <int>, "category": <str> } ], "mixes": { "ARTIST_MIX": <str> } } ] }, "albums": { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "premiumStreamingOnly": <bool>, "numberOfTracks": <int>, "numberOfVideos": <int>, "numberOfVolumes": <int>, "releaseDate": <str>, "copyright": <str>, "type": "ALBUM", "version": <str>, "url": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str>, "explicit": <bool>, "upc": <str>, "popularity": <int>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ] } ] }, "playlists": { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "uuid": <str>, "title": <str>, "numberOfTracks": <int>, "numberOfVideos": <int>, "creator": { "id": <int> }, "description": <str>, "duration": <int>, "lastUpdated": <str>, "created": <str>, "type": <str>, "publicPlaylist": <bool>, "url": <str>, "image": <str>, "popularity": <int>, "squareImage": <str>, "promotedArtists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str>, } ], "lastItemAddedAt": <str> } ] }, "tracks": { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": <int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } } ] }, "videos": { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "volumeNumber": <int>, "trackNumber": <int>, "releaseDate": <str>, "imagePath": <str>, "imageId": <str>, "vibrantColor": <str>, "duration": <int>, "quality": <str>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "explicit": <bool>, "popularity": <int>, "type": <str>, "adsUrl": <str>, "adsPrePaywallOnly": <bool>, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str>, }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str>, } ], "album": <dict> } ] }, "topHit": { "value": <dict>, "type": <str> } } """ self._check_scope( "search", "r_usr", flows={"device_code"}, require_authentication=False, ) url = f"{self.API_URL}/v1/search" if type: if type not in ( TYPES := { "artist", "album", "playlist", "track", "userProfile", "video", } ): emsg = ( "Invalid target search type. Valid values: " f"{', '.join(TYPES)}." ) raise ValueError(emsg) url += f"/{type}s" return self._get_json( url, params={ "query": query, "limit": limit, "offset": offset, "countryCode": self._get_country_code(country_code), }, )
### STREAMS ###############################################################
[docs] def get_collection_streams( self, collection_id: Union[int, str], type: str, *, audio_quality: str = "HI_RES", video_quality: str = "HIGH", max_resolution: int = 2160, playback_mode: str = "STREAM", asset_presentation: str = "FULL", streaming_session_id: str = None, ) -> list[tuple[bytes, str]]: """ Get audio and video stream data for items (tracks and videos) in an album, mix, or playlist. .. admonition:: User authentication, authorization scope, and subscription :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Full track and video playback information and lossless audio is only available with user authentication and an active TIDAL subscription. High-resolution and immersive audio is only available with the HiFi Plus plan and when the current client credentials are from a supported device. .. seealso:: For more information on audio quality availability, see the `Download TIDAL <https://offer.tidal.com/download>`_, `TIDAL Pricing <https://tidal.com/pricing>`_, and `Dolby Atmos <https://support.tidal.com/hc/en-us/articles /360004255778-Dolby-Atmos>`_ web pages. .. note:: This method is provided for convenience and is not a private TIDAL API endpoint. Parameters ---------- collection_id : `int` or `str` TIDAL collection ID or UUID. type : `str` Collection type. **Valid values**: :code:`"album"`, :code:`"mix"`, and :code:`"playlist"`. audio_quality : `str`, keyword-only, default: :code:`"HI-RES"` Audio quality. .. container:: **Valid values**: * :code:`"LOW"` for 64 kbps (22.05 kHz) MP3 without user authentication or 96 kbps AAC with user authentication. * :code:`"HIGH"` for 320 kbps AAC. * :code:`"LOSSLESS"` for 1411 kbps (16-bit, 44.1 kHz) ALAC or FLAC. * :code:`"HI_RES"` for up to 9216 kbps (24-bit, 96 kHz) MQA-encoded FLAC. video_quality : `str`, keyword-only, default: :code:`"HIGH"` Video quality. **Valid values**: :code:`"AUDIO_ONLY"`, :code:`"LOW"`, :code:`"MEDIUM"`, and :code:`"HIGH"`. max_resolution : `int`, keyword-only, default: :code:`2160` Maximum video resolution (number of vertical pixels). playback_mode : `str`, keyword-only, default: :code:`"STREAM"` Playback mode. **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`. asset_presentation : `str`, keyword-only, default: :code:`"FULL"` Asset presentation. .. container:: **Valid values**: * :code:`"FULL"`: Full track or video. * :code:`"PREVIEW"`: 30-second preview of the track or video. streaming_session_id : `str`, keyword-only, optional Streaming session ID. Returns ------- streams : `list` Audio and video stream data and their MIME types. """ if type not in (COLLECTION_TYPES := {"album", "mix", "playlist"}): emsg = ( "Invalid collection type. Valid values: " f"{', '.join(COLLECTION_TYPES)}." ) raise ValueError(emsg) if type == "album": items = self.get_album_items(collection_id)["items"] elif type == "mix": items = self.get_mix_items(collection_id)["items"] elif type == "playlist": items = self.get_playlist_items(collection_id)["items"] streams = [] for item in items: if item["type"] == "track": stream = self.get_track_stream( item["item"]["id"], audio_quality=audio_quality, playback_mode=playback_mode, asset_presentation=asset_presentation, streaming_session_id=streaming_session_id, ) elif item["type"] == "video": stream = self.get_video_stream( item["item"]["id"], video_quality=video_quality, max_resolution=max_resolution, playback_mode=playback_mode, asset_presentation=asset_presentation, streaming_session_id=streaming_session_id, ) streams.append(stream) return streams
[docs] def get_track_stream( self, track_id: Union[int, str], *, audio_quality: str = "HI_RES_LOSSLESS", playback_mode: str = "STREAM", asset_presentation: str = "FULL", streaming_session_id: str = None, ) -> Union[bytes, str]: """ Get the audio stream data for a track. .. admonition:: User authentication, authorization scope, and subscription :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Full track playback information and lossless audio is only available with user authentication and an active TIDAL subscription. High-resolution and immersive audio is only available with the HiFi Plus plan and when the current client credentials are from a supported device. .. seealso:: For more information on audio quality availability, see the `Download TIDAL <https://offer.tidal.com/download>`_, `TIDAL Pricing <https://tidal.com/pricing>`_, and `Dolby Atmos <https://support.tidal.com/hc/en-us/articles /360004255778-Dolby-Atmos>`_ web pages. .. note:: This method is provided for convenience and is not a private TIDAL API endpoint. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. audio_quality : `str`, keyword-only, default: :code:`"HI-RES"` Audio quality. .. container:: **Valid values**: * :code:`"LOW"` for 64 kbps (22.05 kHz) MP3 without user authentication or 96 kbps AAC with user authentication. * :code:`"HIGH"` for 320 kbps AAC. * :code:`"LOSSLESS"` for 1411 kbps (16-bit, 44.1 kHz) ALAC or FLAC. * :code:`"HI_RES"` for up to 9216 kbps (24-bit, 96 kHz) MQA-encoded FLAC. playback_mode : `str`, keyword-only, default: :code:`"STREAM"` Playback mode. **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`. asset_presentation : `str`, keyword-only, default: :code:`"FULL"` Asset presentation. .. container:: **Valid values**: * :code:`"FULL"`: Full track. * :code:`"PREVIEW"`: 30-second preview of the track. streaming_session_id : `str`, keyword-only, optional Streaming session ID. Returns ------- stream : `bytes` Audio stream data. codec : `str` Audio codec. """ manifest = base64.b64decode( self.get_track_playback_info( track_id, audio_quality=audio_quality, playback_mode=playback_mode, asset_presentation=asset_presentation, streaming_session_id=streaming_session_id, )["manifest"] ) if b"urn:mpeg:dash" in manifest: manifest = minidom.parseString(manifest) codec = manifest.getElementsByTagName("Representation")[ 0 ].getAttribute("codecs") segment = manifest.getElementsByTagName("SegmentTemplate")[0] stream = bytearray() with self.session.get(segment.getAttribute("initialization")) as r: stream.extend(r.content) for i in range( 1, sum( int(tl.getAttribute("r") or 1) for tl in segment.getElementsByTagName("S") ) + 2, ): with self.session.get( segment.getAttribute("media").replace("$Number$", str(i)) ) as r: stream.extend(r.content) else: manifest = json.loads(manifest) codec = manifest["codecs"] with self.session.get(manifest["urls"][0]) as r: stream = r.content if manifest["encryptionType"] == "OLD_AES": key_id = base64.b64decode(manifest["keyId"]) key_nonce = ( Cipher( algorithms.AES( b"P\x89SLC&\x98\xb7\xc6\xa3\n?P.\xb4\xc7" b"a\xf8\xe5n\x8cth\x13E\xfa?\xbah8\xef\x9e" ), modes.CBC(key_id[:16]), ) .decryptor() .update(key_id[16:]) ) stream = ( Cipher( algorithms.AES(key_nonce[:16]), modes.CTR(key_nonce[16:32]), ) .decryptor() .update(stream) ) elif manifest["encryptionType"] != "NONE": raise NotImplementedError("Unsupported encryption type.") return stream, codec
[docs] def get_video_stream( self, video_id: Union[int, str], *, video_quality: str = "HIGH", max_resolution: int = 2160, playback_mode: str = "STREAM", asset_presentation: str = "FULL", streaming_session_id: str = None, ) -> tuple[bytes, str]: """ Get the video stream data for a music video. .. admonition:: User authentication, authorization scope, and subscription :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Full video playback information is only available with user authentication and an active TIDAL subscription. .. note:: This method is provided for convenience and is not a private TIDAL API endpoint. Parameters ---------- video_id : `int` or `str` TIDAL video ID. **Example**: :code:`59727844`. video_quality : `str`, keyword-only, default: :code:`"HIGH"` Video quality. **Valid values**: :code:`"AUDIO_ONLY"`, :code:`"LOW"`, :code:`"MEDIUM"`, and :code:`"HIGH"`. max_resolution : `int`, keyword-only, default: :code:`2160` Maximum video resolution (number of vertical pixels). playback_mode : `str`, keyword-only, default: :code:`"STREAM"` Playback mode. **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`. asset_presentation : `str`, keyword-only, default: :code:`"FULL"` Asset presentation. .. container:: **Valid values**: * :code:`"FULL"`: Full video. * :code:`"PREVIEW"`: 30-second preview of the video. streaming_session_id : `str`, keyword-only, optional Streaming session ID. Returns ------- stream : `bytes` Video stream data. codec : `str` Video codec. """ manifest = base64.b64decode( self.get_video_playback_info( video_id, video_quality=video_quality, playback_mode=playback_mode, asset_presentation=asset_presentation, streaming_session_id=streaming_session_id, )["manifest"] ) codec, playlist = next( (c, pl) for c, res, pl in re.findall( r'(?<=CODECS=")(.*)",(?:RESOLUTION=)\d+x(\d+)\n(http.*)', self.session.get( json.loads(manifest)["urls"][0] ).content.decode("utf-8"), )[::-1] if int(res) < max_resolution ) stream = bytearray() for ts in re.findall( "(?<=\n).*(http.*)", self.session.get(playlist).content.decode("utf-8"), ): with self.session.get(ts) as r: stream.extend(r.content) return stream, codec
### TRACKS ################################################################
[docs] def get_track( self, track_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for a track. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- track : `dict` TIDAL catalog information for a track. .. admonition:: Sample response :class: dropdown .. code:: { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": <int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } } """ self._check_scope( "get_track", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/tracks/{track_id}", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_track_contributors( self, track_id: Union[int, str], country_code: str = None, *, limit: int = None, offset: int = None, ) -> dict[str, Any]: """ Get the contributors to a track and their roles. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`100` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- contributors : `dict` A dictionary containing a track's contributors and their roles, and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "name": <str>, "role": <str> } ] } """ self._check_scope( "get_track_contributors", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/tracks/{track_id}/contributors", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_track_credits( self, track_id: Union[int, str], country_code: str = None ) -> list[dict[str, Any]]: """ Get credits for a track. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_id : `int` or `str` TIDAL track ID. country : `str`, keyword-only, optional An ISO 3166-1 alpha-2 country code. If not specified, the country associated with the user account will be used. **Example**: :code:`"US"`. Returns ------- credits : `list` A list of roles and their associated contributors. .. admonition:: Sample response :class: dropdown .. code:: [ { "type": <str>, "contributors": [ { "name": <str>, "id": <int> } ] } ] """ self._check_scope( "get_track_credits", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/tracks/{track_id}/credits", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_track_composers(self, track_id: Union[int, str]) -> list[str]: """ Get the composers, lyricists, and/or songwriters of a track. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. .. note:: This method is provided for convenience and is not a private TIDAL API endpoint. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. Returns ------- composers : `list` Composers, lyricists, and/or songwriters of the track. **Example**: :code:`['Tommy Wright III', 'Beyoncé', 'Kelman Duran', 'Terius "The-Dream" G...de-Diamant', 'Mike Dean']` """ return sorted( { c["name"] for c in self.get_track_contributors(track_id)["items"] if c["role"] in {"Composer", "Lyricist", "Writer"} } )
[docs] def get_track_lyrics( self, track_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get lyrics for a track. .. admonition:: User authentication and subscription :class: warning Requires user authentication via an OAuth 2.0 authorization flow and an active TIDAL subscription. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- lyrics : `dict` A dictionary containing formatted and time-synced lyrics (if available) in the `"lyrics"` and `"subtitles"` keys, respectively. .. admonition:: Sample response :class: dropdown .. code:: { "trackId": <int>, "lyricsProvider": <str>, "providerCommontrackId": <str>, "providerLyricsId": <str>, "lyrics": <str>, "subtitles": <str>, "isRightToLeft": <bool> } """ self._check_scope("get_track_lyrics") try: return self._get_json( f"{self.WEB_URL}/v1/tracks/{track_id}/lyrics", params={"countryCode": self._get_country_code(country_code)}, ) except RuntimeError: logging.warning( "Either lyrics are not available for this track " "or the current account does not have an active " "TIDAL subscription." )
[docs] def get_track_mix_id( self, track_id: Union[int, str], country_code: str = None ) -> str: """ Get the curated mix of tracks based on a track. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- mix_id : `str` TIDAL mix ID. **Example**: :code:`"0017159e6a1f34ae3d981792d72ecf"`. """ self._check_scope( "get_track_mix_id", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/tracks/{track_id}/mix", params={"countryCode": self._get_country_code(country_code)}, )["id"]
[docs] def get_track_playback_info( self, track_id: Union[int, str], *, audio_quality: str = "HI_RES_LOSSLESS", playback_mode: str = "STREAM", asset_presentation: str = "FULL", streaming_session_id: str = None, ) -> dict[str, Any]: """ Get playback information for a track. .. admonition:: User authentication, authorization scope, and subscription :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Full track playback information and lossless audio is only available with user authentication and an active TIDAL subscription. High-resolution and immersive audio is only available with the HiFi Plus plan and when the current client credentials are from a supported device. .. seealso:: For more information on audio quality availability, see the `Download TIDAL <https://offer.tidal.com/download>`_, `TIDAL Pricing <https://tidal.com/pricing>`_, and `Dolby Atmos <https://support.tidal.com/hc/en-us/articles /360004255778-Dolby-Atmos>` web pages. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. audio_quality : `str`, keyword-only, default: :code:`"HI_RES"` Audio quality. .. container:: **Valid values**: * :code:`"LOW"` for 64 kbps (22.05 kHz) MP3 without user authentication or 96 kbps AAC with user authentication. * :code:`"HIGH"` for 320 kbps AAC. * :code:`"LOSSLESS"` for 1411 kbps (16-bit, 44.1 kHz) ALAC or FLAC. * :code:`"HI_RES"` for up to 9216 kbps (24-bit, 96 kHz) MQA-encoded FLAC. playback_mode : `str`, keyword-only, default: :code:`"STREAM"` Playback mode. **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`. asset_presentation : `str`, keyword-only, default: :code:`"FULL"` Asset presentation. .. container:: **Valid values**: * :code:`"FULL"`: Full track. * :code:`"PREVIEW"`: 30-second preview of the track. streaming_session_id : `str`, keyword-only, optional Streaming session ID. Returns ------- info : `dict` Track playback information. .. admonition:: Sample response :class: dropdown .. code:: { "trackId": <int>, "assetPresentation": <str>, "audioMode": <str>, "audioQuality": <str>, "manifestMimeType": <str>, "manifestHash": <str>, "manifest": <str>, "albumReplayGain": <float>, "albumPeakAmplitude": <float>, "trackReplayGain": <float>, "trackPeakAmplitude": <float> } """ self._check_scope( "get_track_playback_info", "r_usr", flows={"device_code"}, require_authentication=False, ) if audio_quality not in ( AUDIO_QUALITIES := { "LOW", "HIGH", "LOSSLESS", "HI_RES", "HI_RES_LOSSLESS", } ): emsg = ( "Invalid audio quality. Valid values: " f"are{', '.join(AUDIO_QUALITIES)}." ) raise ValueError(emsg) if playback_mode not in (PLAYBACK_MODES := {"STREAM", "OFFLINE"}): emsg = ( "Invalid playback mode. Valid values: " f"modes are {', '.join(PLAYBACK_MODES)}." ) raise ValueError(emsg) if asset_presentation not in ( ASSET_PRESENTATIONS := {"FULL", "PREVIEW"} ): emsg = ( "Invalid asset presentation. Valid values: " "presentations are " f"{', '.join(ASSET_PRESENTATIONS)}." ) raise ValueError(emsg) url = f"{self.API_URL}/v1/tracks/{track_id}/playbackinfo" # if self._flow: # url += "postpaywall" url += "postpaywall" if self._flow else "prepaywall" return self._get_json( url, params={ "audioquality": audio_quality, "assetpresentation": asset_presentation, "playbackmode": playback_mode, "streamingsessionid": streaming_session_id, }, )
[docs] def get_track_recommendations( self, track_id: Union[int, str], country_code: str = None, *, limit: int = None, offset: int = None, ) -> dict[str, Any]: """ Get TIDAL catalog information for a track's recommended tracks and videos. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_id : `int` or `str` TIDAL track ID. **Example**: :code:`251380837`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, optional Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- recommendations : `dict` A dictionary containing TIDAL catalog information for the recommended tracks and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": <int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } }, "sources": [ "SUGGESTED_TRACKS" ] ] } """ self._check_scope( "get_track_recommendations", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/tracks/{track_id}/recommendations", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, }, )
[docs] def get_favorite_tracks( self, country_code: str = None, *, limit: int = 50, offset: int = None, order: str = "DATE", order_direction: str = "DESC", ): """ Get TIDAL catalog information for tracks in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. order : `str`, keyword-only, default: :code:`"DATE"` Sorting order. **Valid values**: :code:`"DATE"` and :code:`"NAME"`. order_direction : `str`, keyword-only, default: :code:`"DESC"` Sorting order direction. **Valid values**: :code:`"DESC"` and :code:`"ASC"`. Returns ------- tracks : `dict` A dictionary containing TIDAL catalog information for tracks in the current user's collection and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "created": <str>, "item": { "id": <int>, "title": <str>, "duration": <int>, "replayGain": <float>, "peak": <float>, "allowStreaming": <bool>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "premiumStreamingOnly": <bool>, "trackNumber": <int>, "volumeNumber": <int>, "version": <str>, "popularity": <int>, "copyright": <str>, "url": <str>, "isrc": <str>, "editable": <bool>, "explicit": <bool>, "audioQuality": <str>, "audioModes": [<str>], "mediaMetadata": { "tags": [<str>] }, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str> } ], "album": { "id": <int>, "title": <str>, "cover": <str>, "vibrantColor": <str>, "videoCover": <str> }, "mixes": { "TRACK_MIX": <str> } } } ] } """ self._check_scope( "get_favorite_tracks", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/favorites/tracks", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, "order": order, "orderDirection": order_direction, }, )
[docs] def favorite_tracks( self, track_ids: Union[int, str, list[Union[int, str]]], country_code: str = None, *, on_artifact_not_found: str = "FAIL", ) -> None: """ Add tracks to the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_ids : `int`, `str`, or `list` TIDAL track ID(s). **Examples**: :code:`"251380837,251380838"` or :code:`[251380837, 251380838]`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added does not exist. **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`. """ self._check_scope("favorite_tracks", "r_usr", flows={"device_code"}) self._request( "post", f"{self.API_URL}/v1/users/{self._user_id}/favorites/tracks", params={"countryCode": self._get_country_code(country_code)}, data={ "trackIds": ( ",".join(map(str, track_ids)) if isinstance(track_ids, list) else track_ids ), "onArtifactNotFound": on_artifact_not_found, }, )
[docs] def unfavorite_tracks( self, track_ids: Union[int, str, list[Union[int, str]]] ) -> None: """ Remove tracks from the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- track_ids : `int`, `str`, or `list` TIDAL track ID(s). **Examples**: :code:`"251380837,251380838"` or :code:`[251380837, 251380838]`. """ self._check_scope("unfavorite_tracks", "r_usr", flows={"device_code"}) if isinstance(track_ids, list): track_ids = ",".join(map(str, track_ids)) self._request( "delete", f"{self.API_URL}/v1/users/{self._user_id}" f"/favorites/tracks/{track_ids}", )
### USERS #################################################################
[docs] def get_profile(self) -> dict[str, Any]: """ Get the current user's profile information. .. admonition:: User authentication :class: warning Requires user authentication via an OAuth 2.0 authorization flow. Returns ------- profile : `dict` A dictionary containing the current user's profile information. .. admonition:: Sample response :class: dropdown .. code:: { "userId": <int>, "email": <str>, "countryCode": <str>, "fullName": <str>, "firstName": <str>, "lastName": <str>, "nickname": <str>, "username": <str>, "address": <str>, "city": <str>, "postalcode": <str>, "usState": <str>, "phoneNumber": <int>, "birthday": <int>, "channelId": <int>, "parentId": <int>, "acceptedEULA": <bool>, "created": <int>, "updated": <int>, "facebookUid": <int>, "appleUid": <int>, "googleUid": <int>, "accountLinkCreated": <bool>, "emailVerified": <bool>, "newUser": <bool> } """ self._check_scope("get_profile") return self._get_json(f"{self.LOGIN_URL}/oauth2/me")
[docs] def get_session(self) -> dict[str, Any]: """ Get information about the current private TIDAL API session. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Returns ------- session : `dict` Information about the current private TIDAL API session. .. admonition:: Sample response :class: dropdown .. code:: { "sessionId": <str>, "userId": <int>, "countryCode": <str>, "channelId": <int>, "partnerId": <int>, "client": { "id": <int>, "name": <str>, "authorizedForOffline": <bool>, "authorizedForOfflineDate": <str> } } """ self._check_scope("get_session", "r_usr", flows={"device_code"}) return self._get_json(f"{self.API_URL}/v1/sessions")
[docs] def get_favorite_ids(self) -> dict[str, list[str]]: """ Get TIDAL IDs or UUIDs of the albums, artists, playlists, tracks, and videos in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Returns ------- ids : `dict` A dictionary containing the IDs or UUIDs of the items in the current user's collection. .. admonition:: Sample response :class: dropdown .. code:: { "ARTIST": [<str>], "ALBUM": [<str>], "VIDEO": [<str>], "PLAYLIST": [<str>], "TRACK": [<str>] } """ self._check_scope("get_favorite_ids", "r_usr", flows={"device_code"}) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/favorites/ids" )
[docs] def get_user_profile(self, user_id: Union[int, str]) -> dict[str, Any]: """ Get a TIDAL user's profile information. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `int` or `str` TIDAL user ID. **Example**: :code:`172311284`. Returns ------- profile : `dict` A dictionary containing the user's profile information. .. admonition:: Sample response :class: dropdown .. code:: { "userId": <int>, "name": <str>, "color": [<str>], "picture": <str>, "numberOfFollowers": <int>, "numberOfFollows": <int>, "prompts": [ { "id": <int>, "title": <str>, "description": <str>, "colors": { "primary": <str>, "secondary": <str>, }, "trn": <str>, "data": <str>, "updatedTime": <str>, "supportedContentType": "TRACK" } ], "profileType": <str> } """ self._check_scope("get_user_profile", "r_usr", flows={"device_code"}) return self._get_json(f"{self.API_URL}/v2/profiles/{user_id}")
[docs] def get_user_followers( self, user_id: Union[int, str] = None, *, limit: int = 500, cursor: str = None, ) -> dict[str, Any]: """ Get a TIDAL user's followers. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `str` TIDAL user ID. If not specified, the ID associated with the user account in the current session is used. **Example**: :code:`172311284`. limit : `int`, keyword-only, default: :code:`500` Page size. **Example**: :code:`10`. cursor : `str`, keyword-only, optional Cursor position of the last item in previous search results. Use with `limit` to get the next page of search results. Returns ------- followers : `dict` A dictionary containing the user's followers and the cursor position. """ self._check_scope("get_user_followers", "r_usr", flows={"device_code"}) if user_id is None: user_id = self._user_id return self._get_json( f"{self.API_URL}/v2/profiles/{user_id}/followers", params={"limit": limit, "cursor": cursor}, )
[docs] def get_user_following( self, user_id: Union[int, str] = None, *, include_only: str = None, limit: int = 500, cursor: str = None, ): """ Get the people (artists, users, etc.) a TIDAL user follows. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `str` TIDAL user ID. If not specified, the ID associated with the user account in the current session is used. **Example**: :code:`172311284`. include_only : `str`, keyword-only, optional Type of people to return. **Valid values**: :code:`"ARTIST"` and :code:`"USER"`. limit : `int`, keyword-only, default: :code:`500` Page size. **Example**: :code:`10`. cursor : `str`, keyword-only, optional Cursor position of the last item in previous search results. Use with `limit` to get the next page of search results. Returns ------- following : `dict` A dictionary containing the people following the user and the cursor position. .. admonition:: Sample response :class: dropdown .. code:: { "items": [ { "id": <int>, "name": <str>, "picture": <str>, "imFollowing": <bool>, "trn": <str>, "followType": <str> } ], "cursor": <str> } """ self._check_scope("get_user_following", "r_usr", flows={"device_code"}) if include_only and include_only not in ( ALLOWED_INCLUDES := {"ARTIST", "USER"} ): emsg = ( "Invalid include type. Valid values: " f"{', '.join(ALLOWED_INCLUDES)}." ) raise ValueError(emsg) if user_id is None: user_id = self._user_id return self._get_json( f"{self.API_URL}/v2/profiles/{user_id}/following", params={ "includeOnly": include_only, "limit": limit, "cursor": cursor, }, )
[docs] def follow_user(self, user_id: Union[int, str]) -> None: """ Follow a user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `int` or `str` TIDAL user ID. **Example**: :code:`172311284`. """ self._check_scope("follow_user", "r_usr", flows={"device_code"}) self._request( "put", f"{self.API_URL}/v2/follow", params={"trn": f"trn:user:{user_id}"}, )
[docs] def unfollow_user(self, user_id: Union[int, str]) -> None: """ Unfollow a user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `int` or `str` TIDAL user ID. **Example**: :code:`172311284`. """ self._check_scope("unfollow_user", "r_usr", flows={"device_code"}) self._request( "delete", f"{self.API_URL}/v2/follow", params={"trn": f"trn:user:{user_id}"}, )
[docs] def get_blocked_users( self, *, limit: int = None, offset: int = None ) -> dict[str, Any]: """ Get users blocked by the current user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- limit : `int`, keyword-only, optional Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. Returns ------- users : `dict` A dictionary containing the users blocked by the current user and the number of results. """ self._check_scope("get_blocked_users", "r_usr", flows={"device_code"}) return self._get_json( f"{self.API_URL}/v2/profiles/blocked-profiles", params={"limit": limit, "offset": offset}, )
[docs] def block_user(self, user_id: Union[int, str]) -> None: """ Block a user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `int` or `str` TIDAL user ID. **Example**: :code:`172311284`. """ self._check_scope("block_user", "r_usr", flows={"device_code"}) self._request("put", f"{self.API_URL}/v2/profiles/block/{user_id}")
[docs] def unblock_user(self, user_id: Union[int, str]) -> None: """ Unblock a user. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- user_id : `int` or `str` TIDAL user ID. **Example**: :code:`172311284`. """ self._check_scope("unblock_user", "r_usr", flows={"device_code"}) self._request("delete", f"{self.API_URL}/v2/profiles/block/{user_id}")
### VIDEOS ################################################################
[docs] def get_video( self, video_id: Union[int, str], country_code: str = None ) -> dict[str, Any]: """ Get TIDAL catalog information for a video. .. admonition:: Authorization scope :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- video_id : `int` or `str` TIDAL video ID. **Example**: :code:`59727844`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. Returns ------- video : `dict` TIDAL catalog information for a video. .. admonition:: Sample response :class: dropdown .. code:: { "id": <int>, "title": <str>, "volumeNumber": <int>, "trackNumber": <int>, "releaseDate": <str>, "imagePath": <str>, "imageId": <str>, "vibrantColor": <str>, "duration": <int>, "quality": <str>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "explicit": <bool>, "popularity": <int>, "type": <str>, "adsUrl": <str>, "adsPrePaywallOnly": <bool>, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str>, }, "artists": [ { "id": <int>, "name": <str>, "type": <str>, "picture": <str>, } ], "album": <dict> } """ self._check_scope( "get_video", "r_usr", flows={"device_code"}, require_authentication=False, ) return self._get_json( f"{self.API_URL}/v1/videos/{video_id}", params={"countryCode": self._get_country_code(country_code)}, )
[docs] def get_video_playback_info( self, video_id: Union[int, str], *, video_quality: str = "HIGH", playback_mode: str = "STREAM", asset_presentation: str = "FULL", streaming_session_id: str = None, ) -> dict[str, Any]: """ Get playback information for a video. .. admonition:: User authentication, authorization scope, and subscription :class: dropdown warning Requires the :code:`r_usr` authorization scope if the device code flow was used. Full video playback information is only available with user authentication and an active TIDAL subscription. Parameters ---------- video_id : `int` or `str` TIDAL video ID. **Example**: :code:`59727844`. video_quality : `str`, keyword-only, default: :code:`"HIGH"` Video quality. **Valid values**: :code:`"AUDIO_ONLY"`, :code:`"LOW"`, :code:`"MEDIUM"`, and :code:`"HIGH"`. playback_mode : `str`, keyword-only, default: :code:`"STREAM"` Playback mode. **Valid values**: :code:`"STREAM"` and :code:`"OFFLINE"`. asset_presentation : `str`, keyword-only, default: :code:`"FULL"` Asset presentation. .. container:: **Valid values**: * :code:`"FULL"`: Full video. * :code:`"PREVIEW"`: 30-second preview of the video. streaming_session_id : `str`, keyword-only, optional Streaming session ID. Returns ------- info : `dict` Video playback information. .. admonition:: Sample response :class: dropdown .. code:: { "videoId": <int>, "streamType": <str>, "assetPresentation": <str>, "videoQuality": <str>, "manifestMimeType": <str>, "manifestHash": <str>, "manifest": <str> } """ self._check_scope( "get_video_playback_info", "r_usr", flows={"device_code"}, require_authentication=False, ) if video_quality not in ( VIDEO_QUALITIES := {"AUDIO_ONLY", "LOW", "MEDIUM", "HIGH"} ): emsg = ( "Invalid video quality. Valid values: " f"are{', '.join(VIDEO_QUALITIES)}." ) raise ValueError(emsg) if playback_mode not in (PLAYBACK_MODES := {"STREAM", "OFFLINE"}): emsg = ( "Invalid playback mode. Valid values: " f"modes are {', '.join(PLAYBACK_MODES)}." ) raise ValueError(emsg) if asset_presentation not in ( ASSET_PRESENTATIONS := {"FULL", "PREVIEW"} ): emsg = ( "Invalid asset presentation. Valid values: " "presentations are " f"{', '.join(ASSET_PRESENTATIONS)}." ) raise ValueError(emsg) url = f"{self.API_URL}/v1/videos/{video_id}/playbackinfo" url += "postpaywall" if self._flow else "prepaywall" return self._get_json( url, params={ "videoquality": video_quality, "assetpresentation": asset_presentation, "playbackmode": playback_mode, "streamingsessionid": streaming_session_id, }, )
[docs] def get_favorite_videos( self, country_code: str = None, *, limit: int = 50, offset: int = None, order: str = "DATE", order_direction: str = "DESC", ): """ Get TIDAL catalog information for videos in the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. limit : `int`, keyword-only, default: :code:`50` Page size. **Example**: :code:`10`. offset : `int`, keyword-only, optional Pagination offset (in number of items). **Example**: :code:`0`. order : `str`, keyword-only, default: :code:`"DATE"` Sorting order. **Valid values**: :code:`"DATE"` and :code:`"NAME"`. order_direction : `str`, keyword-only, default: :code:`"DESC"` Sorting order direction. **Valid values**: :code:`"DESC"` and :code:`"ASC"`. Returns ------- videos : `dict` A dictionary containing TIDAL catalog information for videos in the current user's collection and metadata for the returned results. .. admonition:: Sample response :class: dropdown .. code:: { "limit": <int>, "offset": <int>, "totalNumberOfItems": <int>, "items": [ { "created": <str>, "item": { "id": <int>, "title": <str>, "volumeNumber": <int>, "trackNumber": <int>, "releaseDate": <str>, "imagePath": <str>, "imageId": <str>, "vibrantColor": <str>, "duration": <int>, "quality": <str>, "streamReady": <bool>, "adSupportedStreamReady": <bool>, "djReady": <bool>, "stemReady": <bool>, "streamStartDate": <str>, "allowStreaming": <bool>, "explicit": <bool>, "popularity": <int>, "type": <str>, "adsUrl": <str>, "adsPrePaywallOnly": <bool>, "artist": { "id": <int>, "name": <str>, "type": <str>, "picture": <str> }, "artists": [ { "id": <int>, "name": "<str>, "type": <str>, "picture": <str> } ], "album": <dict> } } ] } """ self._check_scope( "get_favorite_videos", "r_usr", flows={"device_code"} ) return self._get_json( f"{self.API_URL}/v1/users/{self._user_id}/favorites/videos", params={ "countryCode": self._get_country_code(country_code), "limit": limit, "offset": offset, "order": order, "orderDirection": order_direction, }, )
[docs] def favorite_videos( self, video_ids: Union[int, str, list[Union[int, str]]], country_code: str = None, *, on_artifact_not_found: str = "FAIL", ) -> None: """ Add videos to the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- video_ids : `int`, `str`, or `list` TIDAL video ID(s). **Examples**: :code:`"59727844,75623239"` or :code:`[59727844, 75623239]`. country_code : `str`, optional ISO 3166-1 alpha-2 country code. If not provided, the country code associated with the user account in the current session or the current IP address will be used instead. **Example**: :code:`"US"`. on_artifact_not_found : `str`, keyword-only, default: :code:`"FAIL"` Behavior when the item to be added does not exist. **Valid values**: :code:`"FAIL"` or :code:`"SKIP"`. """ self._check_scope("favorite_videos", "r_usr", flows={"device_code"}) self._request( "post", f"{self.API_URL}/v1/users/{self._user_id}/favorites/videos", params={"countryCode": self._get_country_code(country_code)}, data={ "videoIds": ( ",".join(map(str, video_ids)) if isinstance(video_ids, list) else video_ids ), "onArtifactNotFound": on_artifact_not_found, }, )
[docs] def unfavorite_videos( self, video_ids: Union[int, str, list[Union[int, str]]] ) -> None: """ Remove videos from the current user's collection. .. admonition:: User authentication and authorization scope :class: warning Requires user authentication and the :code:`r_usr` authorization scope if the device code flow was used. Parameters ---------- video_ids : `int`, `str`, or `list` TIDAL video ID(s). **Examples**: :code:`"59727844,75623239"` or :code:`[59727844, 75623239]`. """ self._check_scope("unfavorite_videos", "r_usr", flows={"device_code"}) if isinstance(video_ids, list): video_ids = ",".join(map(str, video_ids)) self._request( "delete", f"{self.API_URL}/v1/users/{self._user_id}" f"/favorites/videos/{video_ids}", )