Skip to content
Merged
9 changes: 8 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

History
=======
v0.8.6
------
* Add support for get<track, album, artist, playlist>count(), Workers: Use get_*_count to get the actual number of items. - tehkillerbee_
* Only return warning if page itemtype (v2) is not implemented (Fixes: #362) - tehkillerbee_
* Add legacy home endpoint for backwards compatibility - tehkillerbee_
* Get playlist tracks, items count. Get playlist tracks paginated. - tehkillerbee_

v0.8.5
------
* Cleanup: Removed deprecated function(s). - tehkillerbee_
* Feature: MixV2: Add support for parsing mixes originating from PageCategoryV2. - tehkillerbee_
* Feature: Add support for PageCategoryV2 as used on Home page. - tehkillerbee_, Nokse22_
* Feature: Get home page using new v2 endpoint. Add support for PageCategoryV2 - tehkillerbee_, Nokse22_
* Feature: Add pagination workers from mopidy-tidal - tehkillerbee_, BlackLight_
* Fix(playlist): Improve v2 endpoint usage. - tehkillerbee_
* fix(playlist): More robust handling of the passed objects. - BlackLight_
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
author = "The tidalapi Developers"

# The full version, including alpha/beta/rc tags
release = "0.8.5"
release = "0.8.6"


# -- General configuration ---------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[tool.poetry]
name = "tidalapi"
version = "0.8.5"
version = "0.8.6"
description = "Unofficial API for TIDAL music streaming service."
authors = ["Thomas Amland <thomas.amland@googlemail.com>"]
maintainers = ["tehkillerbee <tehkillerbee@users.noreply.github.com>"]
license = "LGPL-3.0-or-later"
readme = ["README.rst", "HISTORY.rst"]
homepage = "https://tidalapi.netlify.app"
repository = "https://github.com/tamland/python-tidal"
repository = "https://github.com/EbbLabs/python-tidal"
documentation = "https://tidalapi.netlify.app"
classifiers = [
"Development Status :: 4 - Beta",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def test_genres(session):
def test_moods(session):
moods = session.moods()
first = next(iter(moods))
assert first.title == "Holidays"
assert first.title == "Holidays" or first.title == "For DJs"
assert isinstance(next(iter(first.get())), tidalapi.Playlist)


Expand Down
2 changes: 1 addition & 1 deletion tidalapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
User,
)

__version__ = "0.8.5"
__version__ = "0.8.6"
10 changes: 8 additions & 2 deletions tidalapi/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""

import copy
import logging
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -65,6 +66,8 @@

AllCategoriesV2 = Union[PageCategoriesV2]

log = logging.getLogger(__name__)


class Page:
"""
Expand Down Expand Up @@ -337,13 +340,16 @@ def __init__(self, session: "Session"):
self.items: List[Any] = []

def parse(self, json_obj: "JsonObj"):
self.items = [self.get_item(item) for item in json_obj["items"]]
self.items = [
self.get_item(item) for item in json_obj["items"] if item is not None
]
return self

def get_item(self, json_obj: "JsonObj") -> Any:
item_type = json_obj.get("type")
if item_type not in self.item_types:
raise NotImplementedError(f"Item type '{item_type}' not implemented")
log.warning(f"Item type '{item_type}' not implemented")
return None

return self.item_types[item_type](json_obj["data"])

Expand Down
49 changes: 49 additions & 0 deletions tidalapi/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from tidalapi.exceptions import ObjectNotFound, TooManyRequests
from tidalapi.types import ItemOrder, JsonObj, OrderDirection
from tidalapi.user import LoggedInUser
from tidalapi.workers import get_items

if TYPE_CHECKING:
from tidalapi.artist import Artist
Expand Down Expand Up @@ -161,6 +162,40 @@ def parse_factory(self, json_obj: JsonObj) -> "Playlist":
self.parse(json_obj)
return copy.copy(self.factory())

def get_tracks_count(
self,
) -> int:
"""Get the total number of tracks in the playlist.

This performs a minimal API request (limit=1) to fetch metadata about the tracks
without retrieving all of them. The API response contains 'totalNumberOfItems',
which represents the total items (tracks) available.
:return: The number of items available.
"""
params = {"limit": 1, "offset": 0}

json_obj = self.request.map_request(
self._base_url % self.id + "/tracks", params=params
)
return json_obj.get("totalNumberOfItems", 0)

def get_items_count(
self,
) -> int:
"""Get the total number of items in the playlist.

This performs a minimal API request (limit=1) to fetch metadata about the tracks
without retrieving all of them. The API response contains 'totalNumberOfItems',
which represents the total items (tracks) available.
:return: The number of items available.
"""
params = {"limit": 1, "offset": 0}

json_obj = self.request.map_request(
self._base_url % self.id + "/items", params=params
)
return json_obj.get("totalNumberOfItems", 0)

def tracks(
self,
limit: Optional[int] = None,
Expand Down Expand Up @@ -195,6 +230,20 @@ def tracks(
)
)

def tracks_paginated(
self,
order: Optional[ItemOrder] = None,
order_direction: Optional[OrderDirection] = None,
) -> List["Playlist"]:
"""Get the tracks in the playlist, using pagination.

:param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user favorite tracks. eg.: "NAME, "DATE"
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
:return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite tracks.
"""
count = self.get_tracks_count()
return get_items(self.tracks, count, order, order_direction)

def items(
self,
limit: int = 100,
Expand Down
26 changes: 15 additions & 11 deletions tidalapi/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,21 +1082,25 @@ def get_user(

return user.User(session=self, user_id=user_id).factory()

def home(self) -> page.Page:
def home(self, use_legacy_endpoint: bool = False) -> page.Page:
"""
Retrieves the Home page, as seen on https://listen.tidal.com
Retrieves the Home page, as seen on https://listen.tidal.com, using either the V2 or V1 (legacy) endpoint

:param use_legacy_endpoint: (Optional) Request Page from legacy endpoint.
:return: A :class:`.Page` object with the :class:`.PageCategory` list from the home page
"""
params = {"deviceType": "BROWSER", "locale": self.locale, "platform": "WEB"}

json_obj = self.request.request(
"GET",
"home/feed/static",
base_url=self.config.api_v2_location,
params=params,
).json()
return self.page.parse(json_obj)
if not use_legacy_endpoint:
params = {"deviceType": "BROWSER", "locale": self.locale, "platform": "WEB"}

json_obj = self.request.request(
"GET",
"home/feed/static",
base_url=self.config.api_v2_location,
params=params,
).json()
return self.page.parse(json_obj)
else:
return self.page.get("pages/home")

def explore(self) -> page.Page:
"""
Expand Down
99 changes: 90 additions & 9 deletions tidalapi/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
def list_validate(lst):
if isinstance(lst, str):
lst = [lst]
if isinstance(lst, int):
lst = [str(lst)]
if len(lst) == 0:
raise ValueError("An empty list was provided.")
return lst
Expand Down Expand Up @@ -555,7 +557,10 @@ def artists_paginated(
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
:return: A :class:`list` :class:`~tidalapi.artist.Artist` objects containing the favorite artists.
"""
return get_items(self.session.user.favorites.artists, order, order_direction)
count = self.session.user.favorites.get_artists_count()
return get_items(
self.session.user.favorites.artists, count, order, order_direction
)

def artists(
self,
Expand Down Expand Up @@ -587,6 +592,21 @@ def artists(
),
)

def get_artists_count(
self,
) -> int:
"""Get the total number of artists in the user's collection.

This performs a minimal API request (limit=1) to fetch metadata about the
artists without retrieving all of them. The API response contains
'totalNumberOfItems', which represents the total items (artists) available.
:return: The number of items available.
"""
params = {"limit": 1, "offset": 0}

json_obj = self.requests.map_request(f"{self.base_url}/artists", params=params)
return json_obj.get("totalNumberOfItems", 0)

def albums_paginated(
self,
order: Optional[AlbumOrder] = None,
Expand All @@ -598,7 +618,10 @@ def albums_paginated(
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
:return: A :class:`list` :class:`~tidalapi.album.Album` objects containing the favorite albums.
"""
return get_items(self.session.user.favorites.albums, order, order_direction)
count = self.session.user.favorites.get_artists_count()
return get_items(
self.session.user.favorites.albums, count, order, order_direction
)

def albums(
self,
Expand Down Expand Up @@ -628,19 +651,36 @@ def albums(
),
)

def get_albums_count(
self,
) -> int:
"""Get the total number of albums in the user's collection.

This performs a minimal API request (limit=1) to fetch metadata about the albums
without retrieving all of them. The API response contains 'totalNumberOfItems',
which represents the total items (albums) available.
:return: The number of items available.
"""
params = {"limit": 1, "offset": 0}

json_obj = self.requests.map_request(f"{self.base_url}/albums", params=params)
return json_obj.get("totalNumberOfItems", 0)

def playlists_paginated(
self,
order: Optional[PlaylistOrder] = None,
order_direction: Optional[OrderDirection] = None,
) -> List["Playlist"]:
"""Get the users favorite playlists relative to the root folder, using
pagination.
"""Get the users favorite playlists, using pagination.

:param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE"
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
:return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite playlists.
"""
return get_items(self.session.user.favorites.playlists, order, order_direction)
count = self.session.user.favorites.get_playlists_count()
return get_items(
self.session.user.favorites.playlists, count, order, order_direction
)

def playlists(
self,
Expand All @@ -666,10 +706,14 @@ def playlists(
}
if order:
params["order"] = order.value
else:
params["order"] = PlaylistOrder.DateCreated.value
if order_direction:
params["orderDirection"] = order_direction.value
else:
params["orderDirection"] = OrderDirection.Descending.value

endpoint = "my-collection/playlists"
endpoint = "my-collection/playlists/folders"
return cast(
List["Playlist"],
self.session.request.map_request(
Expand Down Expand Up @@ -724,19 +768,41 @@ def playlist_folders(
),
)

def get_playlists_count(self) -> int:
"""Get the total number of playlists in the user's root collection.

This performs a minimal API request (limit=1) to fetch metadata about the
playlists without retrieving all of them. The API response contains
'totalNumberOfItems', which represents the total playlists available.
:return: The number of items available.
"""
params = {"folderId": "root", "offset": 0, "limit": 1, "includeOnly": ""}

endpoint = "my-collection/playlists/folders"
json_obj = self.session.request.map_request(
url=urljoin(
self.session.config.api_v2_location,
endpoint,
),
params=params,
)
return json_obj.get("totalNumberOfItems", 0)

def tracks_paginated(
self,
order: Optional[ItemOrder] = None,
order_direction: Optional[OrderDirection] = None,
) -> List["Playlist"]:
"""Get the users favorite playlists relative to the root folder, using
pagination.
"""Get the users favorite tracks, using pagination.

:param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user favorite tracks. eg.: "NAME, "DATE"
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
:return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite tracks.
"""
return get_items(self.session.user.favorites.tracks, order, order_direction)
count = self.session.user.favorites.get_tracks_count()
return get_items(
self.session.user.favorites.tracks, count, order, order_direction
)

def tracks(
self,
Expand Down Expand Up @@ -766,6 +832,21 @@ def tracks(
),
)

def get_tracks_count(
self,
) -> int:
"""Get the total number of tracks in the user's collection.

This performs a minimal API request (limit=1) to fetch metadata about the tracks
without retrieving all of them. The API response contains 'totalNumberOfItems',
which represents the total items (tracks) available.
:return: The number of items available.
"""
params = {"limit": 1, "offset": 0}

json_obj = self.requests.map_request(f"{self.base_url}/tracks", params=params)
return json_obj.get("totalNumberOfItems", 0)

def videos(
self,
limit: Optional[int] = None,
Expand Down
Loading