from __future__ import annotations
from typing import TYPE_CHECKING
from ...._types import COLLECTION_TYPES, ORDERED_COLLECTION_TYPES
from ..._shared import TTLCache, _copy_docstring
from ._shared import PrivateTIDALResourceAPI
from .users import PrivateUsersAPI
if TYPE_CHECKING:
from typing import Any
from ...._types import Collection, OrderedCollection
[docs]
class PrivatePlaylistsAPI(PrivateTIDALResourceAPI):
"""
Playlists API endpoints for the private TIDAL API.
.. important::
This class is managed by
:class:`~minim.api.tidal.PrivateTIDALAPIClient` and should not be
instantiated directly.
"""
_PLAYLIST_TYPES = {
"FOLDER",
"PLAYLIST",
"FAVORITE_PLAYLIST",
"USER_PLAYLIST",
}
_SORT_FIELDS = {"DATE", "NAME"}
__slots__ = ()
@classmethod
def _validate_types(cls, playlist_types: str | Collection[str], /) -> None:
"""
Validate one or more playlist types to filter by.
Parameters
----------
playlist_types : str or Collection[str]; positional-only; optional
Playlist types to return.
"""
if not playlist_types:
raise ValueError("At least one playlist type must be specified.")
if isinstance(playlist_types, str):
cls._validate_types(playlist_types.split(","))
elif isinstance(playlist_types, ORDERED_COLLECTION_TYPES):
for playlist_type in playlist_types:
if playlist_type not in cls._PLAYLIST_TYPES:
raise ValueError(
f"Invalid playlist type {playlist_type!r}. Valid "
f"values: {cls._join_values(cls._PLAYLIST_TYPES)}."
)
else:
raise TypeError(
"`playlist_types` must be a comma-separated string or "
"a collection of strings."
)
def _get_playlist_etag(
self, playlist_uuid: str, /, country_code: str | None = None
) -> str:
"""
Get the entity tag (ETag) for a TIDAL playlist.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
Returns
-------
etag : str
ETag for the playlist.
**Example**: :code:`"1765846447570"`.
"""
self._validate_uuids(playlist_uuid)
if country_code is None:
country_code = self._client._my_country_code
else:
self._validate_country_code(country_code)
return (
self._client._request(
"GET",
f"v1/playlists/{playlist_uuid}",
params={"countryCode": country_code},
)
.headers["etag"]
.replace('"', "")
)
[docs]
@TTLCache.cached_method(ttl="user")
def get_playlist(
self,
playlist_uuid: str,
/,
*,
country_code: str | None = None,
api_version: int = 2,
) -> dict[str, Any]:
"""
Get TIDAL catalog information for a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Conditional
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used. Only applicable when `version` is
:code:`1`.
**Example**: :code:`"US"`.
api_version : int; keyword-only; default: :code:`2`
Private TIDAL API version.
**Valid values**:
* :code:`1` – Legacy
:code:`GET /v1/playlists/{playlist_uuid}` endpoint.
* :code:`2` – Current
:code:`GET /v2/user-playlists/{playlist_uuid}` endpoint.
Returns
-------
playlist : dict[str, Any]
TIDAL metadata for the playlist.
.. admonition:: Sample responses
:class: response dropdown
.. tab-set::
.. tab-item:: Current endpoint
.. code-block::
{
"followInfo": {
"followType": "PLAYLIST",
"followed": <bool>,
"nrOfFollowers": <int>,
"tidalResourceName": <str>
},
"playlist": {
"contentBehavior": <str>,
"created": <str>,
"creator": {
"id": <int>,
"name": <str>,
"picture": <str>,
"type": "USER"
},
"curators": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>
}
],
"customImageUrl": <str>,
"description": <str>,
"duration": <int>,
"image": <str>,
"lastItemAddedAt": <str>,
"lastUpdated": <str>,
"numberOfTracks": <int>,
"numberOfVideos": <int>,
"promotedArtists": [
{
"contributionLinkUrl": <str>,
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>,
"userId": <int>
}
],
"sharingLevel": "PUBLIC",
"source": <str>,
"squareImage": <str>,
"status": <str>,
"title": <str>,
"trn": <str>,
"type": "USER",
"url": <str>,
"uuid": <str>
},
"profile": {
"color": <list[str]>,
"name": <str>,
"userId": <int>
}
}
.. tab-item:: Legacy endpoint
.. code-block::
{
"created": <str>,
"creator": {
"id": <int>
},
"customImageUrl": <str>,
"description": <str>,
"duration": <int>,
"image": <str>,
"lastItemAddedAt": <str>,
"lastUpdated": <str>,
"numberOfTracks": <int>,
"numberOfVideos": <int>,
"popularity": <int>,
"promotedArtists": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
}
],
"publicPlaylist": <bool>,
"squareImage": <str>,
"title": <str>,
"type": <str>,
"url": <str>,
"uuid": <str>
}
"""
self._validate_uuids(playlist_uuid)
self._validate_number("version", api_version, int, 1, 2)
if api_version == 1:
if country_code is None:
country_code = self._client._my_country_code
else:
self._validate_country_code(country_code)
return self._client._request(
"GET",
f"v1/playlists/{playlist_uuid}",
params={"countryCode": country_code},
).json()
self._client._require_authentication("playlists.get_playlist")
return self._client._request(
"GET", f"v2/user-playlists/{playlist_uuid}"
).json()
[docs]
@TTLCache.cached_method(ttl="user")
def get_playlist_items(
self,
playlist_uuid: str,
/,
country_code: str | None = None,
*,
limit: int | None = None,
offset: int | None = None,
) -> dict[str, Any]:
"""
Get TIDAL catalog information for tracks and videos in a
playlist.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
**Example**: :code:`"US"`.
limit : int; keyword-only; optional
Maximum number of items to return.
**Valid range**: :code:`1` to :code:`100`.
**API default**: :code:`10`.
offset : int; keyword-only; optional
Index of the first item to return. Use with `limit` to get
the next set of items.
**Minimum value**: :code:`0`.
**API default**: :code:`0`.
Returns
-------
items : dict[str, Any]
Page of TIDAL metadata for the playlist's tracks and videos.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"items": [
{
"cut": <Any>,
"item": {
"accessType": <str>,
"adSupportedStreamReady": <bool>,
"album": {
"cover": <str>,
"id": <int>,
"releaseDate": <str>,
"title": <str>,
"vibrantColor": <str>,
"videoCover": <str>
},
"allowStreaming": <bool>,
"artist": {
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
},
"artists": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
}
],
"audioModes": <list[str]>,
"audioQuality": <str>,
"bpm": <int>,
"copyright": <str>,
"dateAdded": <str>,
"description": <str>,
"djReady": <bool>,
"duration": <int>,
"editable": <bool>,
"explicit": <bool>,
"id": <int>,
"index": <int>,
"isrc": <str>,
"itemUuid": <str>,
"key": <str>,
"keyScale": <str>,
"mediaMetadata": {
"tags": <list[str]>
},
"mixes": {
"TRACK_MIX": <str>
},
"payToStream": <bool>,
"peak": <float>,
"popularity": <int>,
"premiumStreamingOnly": <bool>,
"replayGain": <float>,
"spotlighted": <bool>,
"stemReady": <bool>,
"streamReady": <bool>,
"streamStartDate": <str>,
"title": <str>,
"trackNumber": <int>,
"upload": <bool>,
"url": <str>,
"version": <str>,
"volumeNumber": <int>
},
"type": "track"
},
{
"cut": <Any>,
"item": {
"adSupportedStreamReady": <bool>,
"adsPrePaywallOnly": <bool>,
"adsUrl": <str>,
"album": {
"cover": <str>,
"id": <int>,
"title": <str>,
"vibrantColor": <str>,
"videoCover": <str>
},
"allowStreaming": <bool>,
"artist": {
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
},
"artists": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
}
],
"dateAdded": <str>,
"djReady": <bool>,
"duration": <int>,
"explicit": <bool>,
"id": <int>,
"imageId": <str>,
"imagePath": <str>,
"index": <int>,
"itemUuid": <str>,
"popularity": <int>,
"quality": <str>,
"releaseDate": <str>,
"stemReady": <bool>,
"streamReady": <bool>,
"streamStartDate": <str>,
"title": <str>,
"trackNumber": <int>,
"type": <str>,
"vibrantColor": <str>,
"volumeNumber": <int>
},
"type": "video"
}
],
"limit": <int>,
"offset": <int>,
"totalNumberOfItems": <int>
}
"""
self._validate_uuids(playlist_uuid)
params = {}
self._client._resolve_country_code(country_code, params)
if limit is not None:
self._validate_number("limit", limit, int, 1, 100)
params["limit"] = limit
if offset is not None:
self._validate_number("offset", offset, int, 0)
params["offset"] = offset
return self._client._request(
"GET", f"v1/playlists/{playlist_uuid}/items", params=params
).json()
[docs]
@TTLCache.cached_method(ttl="user")
def get_playlist_recommended_tracks(
self,
playlist_uuid: str,
/,
country_code: str | None = None,
*,
limit: int | None = None,
offset: int | None = None,
) -> dict[str, Any]:
"""
Get TIDAL catalog information for tracks recommended based on a
playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
**Example**: :code:`"US"`.
limit : int; keyword-only; optional
Maximum number of tracks to return.
**Valid range**: :code:`1` to :code:`100`.
**API default**: :code:`10`.
offset : int; keyword-only; optional
Index of the first track to return. Use with `limit` to get
the next set of tracks.
**Minimum value**: :code:`0`.
**API default**: :code:`0`.
Returns
-------
tracks : dict[str, Any]
Page of TIDAL metadata for the recommended tracks.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"items": [
{
"item": {
"accessType": <str>,
"adSupportedStreamReady": <bool>,
"album": {
"cover": <str>,
"id": <int>,
"releaseDate": <str>,
"title": <str>,
"url": <str>,
"vibrantColor": <str>,
"videoCover": <str>
},
"allowStreaming": <bool>,
"artist": {
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
},
"artists": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
}
],
"audioModes": <list[str]>,
"audioQuality": <str>,
"copyright": <str>,
"description": <str>,
"djReady": <bool>,
"doublePopularity": <float>,
"duration": <int>,
"editable": <bool>,
"explicit": <bool>,
"id": <int>,
"isrc": <str>,
"mediaMetadata": {
"tags": <list[str]>
},
"mixes": {
"TRACK_MIX": <str>
},
"payToStream": <bool>,
"peak": <float>,
"popularity": <int>,
"premiumStreamingOnly": <bool>,
"replayGain": <float>,
"spotlighted": <bool>,
"stemReady": <bool>,
"streamReady": <bool>,
"streamStartDate": <str>,
"title": <str>,
"trackNumber": <int>,
"upload": <bool>,
"url": <str>,
"version": <str>,
"volumeNumber": <int>
},
"type": "track"
}
],
"limit": <int>,
"offset": <int>,
"totalNumberOfItems": <int>
}
"""
self._client._require_authentication(
"playlists.get_playlist_recommended_tracks"
)
self._validate_uuids(playlist_uuid)
params = {}
self._client._resolve_country_code(country_code, params)
if limit is not None:
self._validate_number("limit", limit, int, 1, 100)
params["limit"] = limit
if offset is not None:
self._validate_number("offset", offset, int, 0)
params["offset"] = offset
return self._client._request(
"GET",
f"v1/playlists/{playlist_uuid}/recommendations/items",
params=params,
).json()
[docs]
def create_folder(
self, name: str, *, folder_uuid: str | None = None
) -> dict[str, Any]:
"""
Create a playlist folder.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
name : str
Playlist folder name.
**Example**: :code:`"My New Playlist Folder Title"`.
folder_uuid : str; keyword-only; optional
UUID of TIDAL playlist folder to add the new playlist folder
to. Use :code:`"root"` or leave blank to target the
top-level "Playlists" folder.
Returns
-------
folder : dict[str, Any]
TIDAL metadata for the newly created playlist folder.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"addedAt": <str>,
"data": {
"createdAt": <str>,
"id": <str>,
"itemType": "FOLDER",
"lastModifiedAt": <str>,
"name": <str>,
"totalNumberOfItems": <int>,
"trn": <str>
},
"itemType": "FOLDER",
"lastModifiedAt": <str>,
"name": <str>,
"parent": {
"id": <str>,
"name": <str>
},
"trn": <str>
}
"""
self._client._require_authentication("playlists.create_folder")
params = {"name": self._prepare_string("name", name)}
if folder_uuid is not None:
if folder_uuid != "root":
self._validate_uuids(folder_uuid)
params["folderId"] = folder_uuid
return self._client._request(
"PUT",
"v2/my-collection/playlists/folders/create-folder",
params=params,
).json()
[docs]
def delete_folders(self, folder_uuids: str | Collection[str], /) -> None:
"""
Delete playlist folders.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
folder_uuids : str or Collection[str]; positional-only
UUIDs or TIDAL resource names of the playlist folders.
**Examples**:
* :code:`"trn:folder:618ff600-dce1-4326-8724-9f0a51f63439"`
* :code:`"trn:folder:618ff600-dce1-4326-8724-9f0a51f63439,550e8400-e29b-41d4-a716-446655440000"`
* :code:`["trn:folder:618ff600-dce1-4326-8724-9f0a51f63439",
"550e8400-e29b-41d4-a716-446655440000"]`
"""
self._client._require_authentication("users.delete_folders")
self._client._request(
"PUT",
"v2/my-collection/playlists/folders/remove",
params={
"trns": self._prepare_uuids(
"folder", folder_uuids, has_prefix=True
)
},
)
[docs]
def create_playlist(
self,
name: str,
*,
description: str | None = None,
public: bool | None = None,
folder_uuid: str | None = None,
) -> dict[str, Any]:
"""
Create a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
name : str
Playlist name.
**Example**: :code:`"My New Playlist Title"`.
description : str; keyword-only; optional
Playlist description.
public : bool; keyword-only; optional
Whether the playlist is displayed on the user's profile.
**API default**: :code:`False`.
folder_uuid : str; keyword-only; optional
UUID of TIDAL playlist folder to add the new playlist to.
Use :code:`"root"` or leave blank to target the top-level
"Playlists" folder.
Returns
-------
playlist : dict[str, Any]
TIDAL metadata for the newly created playlist.
.. admonition:: Sample response
:class: response dropdown
.. code-block::
{
"addedAt": <str>,
"data": {
"contentBehavior": <str>,
"created": <str>,
"creator": {
"id": <int>,
"name": <str>,
"picture": <str>,
"type": "USER"
},
"curators": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>
}
],
"customImageUrl": <str>,
"description": <str>,
"duration": 0,
"image": <str>,
"itemType": "PLAYLIST",
"lastItemAddedAt": <str>,
"lastUpdated": <str>,
"numberOfTracks": 0,
"numberOfVideos": 0,
"promotedArtists": [
{
"handle": <str>,
"id": <int>,
"name": <str>,
"picture": <str>,
"type": <str>
}
],
"sharingLevel": <str>,
"source": "DEFAULT",
"squareImage": <str>,
"status": "READY",
"title": <str>,
"trn": <str>,
"type": "USER",
"url": <str>,
"uuid": <str>
},
"itemType": "PLAYLIST",
"lastModifiedAt": <str>,
"name": <str>,
"parent": {
"id": <str>,
"name": <str>
},
"trn": <str>
}
"""
self._client._require_authentication("playlists.create_playlist")
params = {"name": self._prepare_string("name", name)}
if description is not None:
params["description"] = self._prepare_string(
"description", description, allow_blank=True
)
if public is not None:
self._validate_type("public", public, bool)
params["isPublic"] = public
if folder_uuid is not None:
if folder_uuid != "root":
self._validate_uuids(folder_uuid)
params["folderId"] = folder_uuid
return self._client._request(
"PUT",
"v2/my-collection/playlists/folders/create-playlist",
params=params,
).json()
[docs]
def move_playlists(
self,
playlist_uuids: str | Collection[str],
/,
folder_uuid: str | None = None,
) -> None:
"""
Move playlists in the current user's collection.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuids : str or Collection[str]; positional-only
UUIDs or TIDAL resource names of the playlists.
**Examples**:
* :code:`"trn:playlist:0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`
* :code:`"trn:playlist:0ae80812-f8d6-4fc4-90ea-b2df4ecc3861,24c9cc46-2fcd-4afb-bcc6-d6c42315f32e"`
* :code:`["trn:playlist:0ae80812-f8d6-4fc4-90ea-b2df4ecc3861",
"24c9cc46-2fcd-4afb-bcc6-d6c42315f32e"]`
folder_uuid : str
UUID of TIDAL playlist folder to move playlists to. Use
:code:`"root"` or leave blank to target the top-level
"Playlists" folder.
"""
self._client._require_authentication("playlists.move_playlists")
params = {
"trns": self._prepare_uuids(
"playlist", playlist_uuids, has_prefix=True
),
}
if folder_uuid is not None:
if folder_uuid != "root":
self._validate_uuids(folder_uuid)
params["folderId"] = folder_uuid
self._client._request(
"PUT", "v2/my-collection/playlists/folders/move", params=params
)
[docs]
def set_playlist_visibility(
self, playlist_uuid: str, /, public: bool
) -> None:
"""
Set the visibility of a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"36ea71a8-445e-41a4-82ab-6628c581535d"`.
public : bool
Whether the playlist is displayed on the user's profile.
"""
self._client._require_authentication("playlists.set_playlist_privacy")
self._validate_uuids(playlist_uuid)
self._client._request(
"PUT",
f"v2/playlists/{playlist_uuid}/set-"
f"{'public' if public else 'private'}",
)
[docs]
def update_playlist_details(
self,
playlist_uuid: str,
/,
*,
name: str | None = None,
description: str | None = None,
) -> None:
"""
Update the details of a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
.. important::
Either :code:`name` or :code:`description` must be specified.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
name : str; keyword-only; optional
New playlist name.
description : str; keyword-only; optional
New playlist description.
"""
self._client._require_authentication(
"playlists.update_playlist_details"
)
self._validate_uuids(playlist_uuid)
payload = {}
if name is not None:
payload["title"] = self._prepare_string("name", name)
if description is not None:
payload["description"] = self._prepare_string(
"description", description, allow_blank=True
)
if not payload:
raise ValueError("At least one change must be specified.")
self._client._request(
"POST", f"v1/playlists/{playlist_uuid}", data=payload
)
[docs]
def delete_playlists(
self, playlist_uuids: str | Collection[str], /
) -> None:
"""
Delete playlists.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuids : str or Collection[str]; positional-only
UUIDs or TIDAL resource names of the playlists.
**Examples**:
* :code:`"trn:playlist:0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`
* :code:`"trn:playlist:0ae80812-f8d6-4fc4-90ea-b2df4ecc3861,24c9cc46-2fcd-4afb-bcc6-d6c42315f32e"`
* :code:`["trn:playlist:0ae80812-f8d6-4fc4-90ea-b2df4ecc3861",
"24c9cc46-2fcd-4afb-bcc6-d6c42315f32e"]`
"""
self._client._require_authentication("users.delete_playlists")
self._client._request(
"PUT",
"v2/my-collection/playlists/folders/remove",
params={
"trns": self._prepare_uuids(
"playlist", playlist_uuids, has_prefix=True
)
},
)
[docs]
def add_playlist_items(
self,
playlist_uuid: str,
/,
country_code: str | None = None,
*,
item_ids: int | str | Collection[int | str] | None = None,
from_album_id: int | str | None = None,
from_playlist_uuid: str | None = None,
on_duplicate: str | None = None,
) -> None:
"""
Add items to a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
.. important::
Exactly one of `item_ids`, `from_album_id`, or
`from_playlist_uuid` must be provided.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
**Example**: :code:`"US"`.
item_ids : int, str, or Collection[int | str]; keyword-only; \
optional
TIDAL IDs of the tracks and videos.
**Examples**: :code:`46369325`, :code:`"75413016"`,
:code:`"46369325,75413016"`, :code:`[46369325, "75413016"]`.
from_album_id : int or str; keyword-only; optional
TIDAL ID of the album to add tracks and videos from.
**Examples**: :code:`46369321`, :code:`"251380836"`.
from_playlist_uuid : str; keyword-only; optional
UUID of the TIDAL playlist to add tracks and videos
from.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
on_duplicate : str; keyword-only; optional
Behavior when the items to be added are already in the
playlist.
**Valid values**: :code:`"ADD"`, :code:`"FAIL"`,
:code:`"SKIP"`.
"""
self._client._require_authentication("playlists.add_playlist_items")
data = {}
if (
sum(
arg is not None
for arg in (item_ids, from_album_id, from_playlist_uuid)
)
!= 1
):
raise ValueError(
"Exactly one of `item_ids`, `from_album_id`, or "
"`from_playlist_uuid` must be specified."
)
if item_ids is not None:
if isinstance(item_ids, str) and "," in item_ids:
item_ids = item_ids.split(",")
self._validate_tidal_ids(item_ids)
if isinstance(item_ids, COLLECTION_TYPES):
item_ids = ",".join(str(item_id) for item_id in item_ids)
data["itemIds"] = str(item_ids)
elif from_album_id is not None:
self._validate_tidal_ids(from_album_id, recursive=False)
data["fromAlbumId"] = from_album_id
else:
self._validate_uuids(from_playlist_uuid)
data["fromPlaylistUuid"] = from_playlist_uuid
if on_duplicate is not None:
if on_duplicate not in (options := {"ADD", "FAIL", "SKIP"}):
raise ValueError(
f"Invalid duplicate-handling behavior {on_duplicate!r}."
f"Valid values: {self._join_values(options)}."
)
data["onDupes"] = on_duplicate
self._client._request(
"POST",
f"v1/playlists/{playlist_uuid}/items",
data=data,
headers={
"if-none-match": self._get_playlist_etag(
playlist_uuid, country_code=country_code
)
},
)
[docs]
def reorder_playlist_items(
self,
playlist_uuid: str,
/,
from_item_indices: int | str | OrderedCollection[int | str],
to_index: int | str,
country_code: str | None = None,
) -> None:
"""
Reorder items in a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
from_item_indices : int, str, or OrderedCollection[int | str]
Zero-based indices of items to move.
**Examples**: :code:`1`, :code:`"2"`, :code:`"3,4"`,
:code:`[5, "6"]`.
to_index : int or str
Zero-based index to move the items to.
**Examples**: :code:`0`, :code:`"0"`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
**Example**: :code:`"US"`.
"""
self._client._require_authentication(
"playlists.reorder_playlist_items"
)
if isinstance(from_item_indices, str) and "," in from_item_indices:
from_item_indices = from_item_indices.split(",")
self._validate_tidal_ids(from_item_indices)
if isinstance(from_item_indices, ORDERED_COLLECTION_TYPES):
from_item_indices = ",".join(
str(item_idx) for item_idx in from_item_indices
)
self._validate_number("to_index", to_index, int, 0)
self._client._request(
"POST",
f"v1/playlists/{playlist_uuid}/items/{from_item_indices}",
params={"toIndex": to_index},
headers={
"if-none-match": self._get_playlist_etag(
playlist_uuid, country_code=country_code
)
},
)
[docs]
def replace_playlist_item(
self,
playlist_uuid: str,
/,
item_index: int | str,
item_id: int | str,
country_code: str | None = None,
) -> None:
"""
Replace an item in a playlist with another item.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
item_index : int or str
Zero-based index of the item to be replaced.
**Examples**: :code:`1`, :code:`"2"`.
item_id : int or str
TIDAL ID of the track or video to replace the item at the
specified index.
**Examples**: :code:`46369325`, :code:`"75413016"`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
**Example**: :code:`"US"`.
"""
self._client._require_authentication(
"playlists.replace_playlist_items"
)
self._validate_number("item_index", item_index, int, 0)
self._validate_tidal_ids(item_id)
self._client._request(
"POST",
f"v1/playlists/{playlist_uuid}/items/{item_index}/replace",
data={"itemId": item_id},
headers={
"if-none-match": self._get_playlist_etag(
playlist_uuid, country_code=country_code
)
},
)
[docs]
def remove_playlist_items(
self,
playlist_uuid: str,
/,
item_indices: int | str | OrderedCollection[int | str],
country_code: str | None = None,
) -> None:
"""
Remove items from a playlist.
.. admonition:: User authentication
:class: entitlement
.. tab-set::
.. tab-item:: Required
User authentication
Access and manage the user's collection.
Parameters
----------
playlist_uuid : str; positional-only
UUID of the TIDAL playlist.
**Example**: :code:`"0ae80812-f8d6-4fc4-90ea-b2df4ecc3861"`.
item_indices : int, str, or OrderedCollection[int | str]
Zero-based indices of items to remove.
**Examples**: :code:`1`, :code:`"2"`, :code:`"3,4"`,
:code:`[5, "6"]`.
country_code : str; optional
ISO 3166-1 alpha-2 country code. If not provided, the
country associated with the current user account or IP
address is used.
**Example**: :code:`"US"`.
"""
self._client._require_authentication("playlists.remove_playlist_items")
if isinstance(item_indices, str) and "," in item_indices:
item_indices = item_indices.split(",")
self._validate_tidal_ids(item_indices)
if isinstance(item_indices, ORDERED_COLLECTION_TYPES):
item_indices = ",".join(str(item_idx) for item_idx in item_indices)
self._client._request(
"DELETE",
f"v1/playlists/{playlist_uuid}/items/{item_indices}",
headers={
"if-none-match": self._get_playlist_etag(
playlist_uuid, country_code=country_code
)
},
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_followed_playlists)
def get_followed_playlists(
self,
user_id: int | str | None = None,
/,
country_code: str | None = None,
*,
limit: int | None = None,
offset: int | None = None,
sort_by: str | None = None,
descending: bool | None = None,
) -> dict[str, Any]:
return self._client.users.get_followed_playlists(
user_id,
country_code,
limit=limit,
offset=offset,
sort_by=sort_by,
descending=descending,
)
[docs]
@_copy_docstring(PrivateUsersAPI.follow_playlists)
def follow_playlists(
self,
playlist_uuids: str | Collection[str],
/,
*,
user_id: int | str | None = None,
country_code: str | None = None,
folder_uuid: str | None = None,
api_version: int = 2,
) -> None:
self._client.users.follow_playlists(
playlist_uuids,
user_id=user_id,
country_code=country_code,
folder_uuid=folder_uuid,
api_version=api_version,
)
[docs]
@_copy_docstring(PrivateUsersAPI.unfollow_playlists)
def unfollow_playlists(
self,
playlist_uuids: str | Collection[str],
/,
*,
user_id: int | str | None = None,
api_version: int = 2,
) -> None:
self._client.users.unfollow_playlists(
playlist_uuids, user_id=user_id, api_version=api_version
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_my_playlists)
def get_my_playlists(
self,
*,
cursor: str | None = None,
limit: int = 50,
playlist_types: str | Collection[str] | None = None,
sort_by: str | None = None,
descending: bool | None = None,
) -> dict[str, Any]:
return self._client.users.get_my_playlists(
cursor=cursor,
limit=limit,
playlist_types=playlist_types,
sort_by=sort_by,
descending=descending,
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_my_folder)
def get_my_folder(
self,
folder_uuid: str | None = None,
/,
*,
cursor: str | None = None,
limit: int = 50,
playlist_types: str | Collection[str] | None = None,
sort_by: str | None = None,
descending: bool | None = None,
) -> dict[str, Any]:
return self._client.users.get_my_folder(
folder_uuid,
cursor=cursor,
limit=limit,
playlist_types=playlist_types,
sort_by=sort_by,
descending=descending,
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_my_folders_and_playlists)
def get_my_folders_and_playlists(
self,
*,
cursor: str | None = None,
limit: int = 50,
playlist_types: str | Collection[str] | None = None,
sort_by: str | None = None,
descending: bool | None = None,
) -> dict[str, Any]:
return self._client.users.get_my_folders_and_playlists(
cursor=cursor,
limit=limit,
playlist_types=playlist_types,
sort_by=sort_by,
descending=descending,
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_user_playlists)
def get_user_playlists(
self,
user_id: int | str | None = None,
/,
country_code: str | None = None,
*,
limit: int | None = None,
offset: int | None = None,
sort_by: str | None = None,
descending: bool | None = None,
) -> dict[str, Any]:
return self._client.users.get_user_playlists(
user_id,
country_code=country_code,
limit=limit,
offset=offset,
sort_by=sort_by,
descending=descending,
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_user_created_playlists)
def get_user_created_playlists(
self,
user_id: int | str | None = None,
/,
country_code: str | None = None,
*,
limit: int | None = None,
offset: int | None = None,
) -> dict[str, Any]:
return self._client.users.get_user_created_playlists(
user_id, country_code=country_code, limit=limit, offset=offset
)
[docs]
@_copy_docstring(PrivateUsersAPI.get_user_public_playlists)
def get_user_public_playlists(
self,
user_id: int | str | None = None,
/,
*,
cursor: str | None = None,
limit: int | None = None,
) -> dict[str, Any]:
return self._client.users.get_user_public_playlists(
user_id, cursor=cursor, limit=limit
)