Skip to content

Commit 67581e8

Browse files
authored
Merge pull request #364 from EbbLabs/feature/v0.8.6
Feature/v0.8.6
2 parents 998df78 + 944d7cd commit 67581e8

File tree

10 files changed

+198
-65
lines changed

10 files changed

+198
-65
lines changed

HISTORY.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
33
History
44
=======
5+
v0.8.6
6+
------
7+
* Add support for get<track, album, artist, playlist>count(), Workers: Use get_*_count to get the actual number of items. - tehkillerbee_
8+
* Only return warning if page itemtype (v2) is not implemented (Fixes: #362) - tehkillerbee_
9+
* Add legacy home endpoint for backwards compatibility - tehkillerbee_
10+
* Get playlist tracks, items count. Get playlist tracks paginated. - tehkillerbee_
11+
512
v0.8.5
613
------
714
* Cleanup: Removed deprecated function(s). - tehkillerbee_
815
* Feature: MixV2: Add support for parsing mixes originating from PageCategoryV2. - tehkillerbee_
9-
* Feature: Add support for PageCategoryV2 as used on Home page. - tehkillerbee_, Nokse22_
16+
* Feature: Get home page using new v2 endpoint. Add support for PageCategoryV2 - tehkillerbee_, Nokse22_
1017
* Feature: Add pagination workers from mopidy-tidal - tehkillerbee_, BlackLight_
1118
* Fix(playlist): Improve v2 endpoint usage. - tehkillerbee_
1219
* fix(playlist): More robust handling of the passed objects. - BlackLight_

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
author = "The tidalapi Developers"
2424

2525
# The full version, including alpha/beta/rc tags
26-
release = "0.8.5"
26+
release = "0.8.6"
2727

2828

2929
# -- General configuration ---------------------------------------------------

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[tool.poetry]
22
name = "tidalapi"
3-
version = "0.8.5"
3+
version = "0.8.6"
44
description = "Unofficial API for TIDAL music streaming service."
55
authors = ["Thomas Amland <[email protected]>"]
66
maintainers = ["tehkillerbee <[email protected]>"]
77
license = "LGPL-3.0-or-later"
88
readme = ["README.rst", "HISTORY.rst"]
99
homepage = "https://tidalapi.netlify.app"
10-
repository = "https://github.com/tamland/python-tidal"
10+
repository = "https://github.com/EbbLabs/python-tidal"
1111
documentation = "https://tidalapi.netlify.app"
1212
classifiers = [
1313
"Development Status :: 4 - Beta",

tests/test_page.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def test_genres(session):
169169
def test_moods(session):
170170
moods = session.moods()
171171
first = next(iter(moods))
172-
assert first.title == "Holidays"
172+
assert first.title == "Holidays" or first.title == "For DJs"
173173
assert isinstance(next(iter(first.get())), tidalapi.Playlist)
174174

175175

tidalapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717
User,
1818
)
1919

20-
__version__ = "0.8.5"
20+
__version__ = "0.8.6"

tidalapi/page.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""
1919

2020
import copy
21+
import logging
2122
from dataclasses import dataclass
2223
from typing import (
2324
TYPE_CHECKING,
@@ -65,6 +66,8 @@
6566

6667
AllCategoriesV2 = Union[PageCategoriesV2]
6768

69+
log = logging.getLogger(__name__)
70+
6871

6972
class Page:
7073
"""
@@ -337,13 +340,16 @@ def __init__(self, session: "Session"):
337340
self.items: List[Any] = []
338341

339342
def parse(self, json_obj: "JsonObj"):
340-
self.items = [self.get_item(item) for item in json_obj["items"]]
343+
self.items = [
344+
self.get_item(item) for item in json_obj["items"] if item is not None
345+
]
341346
return self
342347

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

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

tidalapi/playlist.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from tidalapi.exceptions import ObjectNotFound, TooManyRequests
2626
from tidalapi.types import ItemOrder, JsonObj, OrderDirection
2727
from tidalapi.user import LoggedInUser
28+
from tidalapi.workers import get_items
2829

2930
if TYPE_CHECKING:
3031
from tidalapi.artist import Artist
@@ -161,6 +162,40 @@ def parse_factory(self, json_obj: JsonObj) -> "Playlist":
161162
self.parse(json_obj)
162163
return copy.copy(self.factory())
163164

165+
def get_tracks_count(
166+
self,
167+
) -> int:
168+
"""Get the total number of tracks in the playlist.
169+
170+
This performs a minimal API request (limit=1) to fetch metadata about the tracks
171+
without retrieving all of them. The API response contains 'totalNumberOfItems',
172+
which represents the total items (tracks) available.
173+
:return: The number of items available.
174+
"""
175+
params = {"limit": 1, "offset": 0}
176+
177+
json_obj = self.request.map_request(
178+
self._base_url % self.id + "/tracks", params=params
179+
)
180+
return json_obj.get("totalNumberOfItems", 0)
181+
182+
def get_items_count(
183+
self,
184+
) -> int:
185+
"""Get the total number of items in the playlist.
186+
187+
This performs a minimal API request (limit=1) to fetch metadata about the tracks
188+
without retrieving all of them. The API response contains 'totalNumberOfItems',
189+
which represents the total items (tracks) available.
190+
:return: The number of items available.
191+
"""
192+
params = {"limit": 1, "offset": 0}
193+
194+
json_obj = self.request.map_request(
195+
self._base_url % self.id + "/items", params=params
196+
)
197+
return json_obj.get("totalNumberOfItems", 0)
198+
164199
def tracks(
165200
self,
166201
limit: Optional[int] = None,
@@ -195,6 +230,20 @@ def tracks(
195230
)
196231
)
197232

233+
def tracks_paginated(
234+
self,
235+
order: Optional[ItemOrder] = None,
236+
order_direction: Optional[OrderDirection] = None,
237+
) -> List["Playlist"]:
238+
"""Get the tracks in the playlist, using pagination.
239+
240+
:param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user favorite tracks. eg.: "NAME, "DATE"
241+
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
242+
:return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite tracks.
243+
"""
244+
count = self.get_tracks_count()
245+
return get_items(self.tracks, count, order, order_direction)
246+
198247
def items(
199248
self,
200249
limit: int = 100,

tidalapi/session.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,21 +1082,25 @@ def get_user(
10821082

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

1085-
def home(self) -> page.Page:
1085+
def home(self, use_legacy_endpoint: bool = False) -> page.Page:
10861086
"""
1087-
Retrieves the Home page, as seen on https://listen.tidal.com
1087+
Retrieves the Home page, as seen on https://listen.tidal.com, using either the V2 or V1 (legacy) endpoint
10881088
1089+
:param use_legacy_endpoint: (Optional) Request Page from legacy endpoint.
10891090
:return: A :class:`.Page` object with the :class:`.PageCategory` list from the home page
10901091
"""
1091-
params = {"deviceType": "BROWSER", "locale": self.locale, "platform": "WEB"}
1092-
1093-
json_obj = self.request.request(
1094-
"GET",
1095-
"home/feed/static",
1096-
base_url=self.config.api_v2_location,
1097-
params=params,
1098-
).json()
1099-
return self.page.parse(json_obj)
1092+
if not use_legacy_endpoint:
1093+
params = {"deviceType": "BROWSER", "locale": self.locale, "platform": "WEB"}
1094+
1095+
json_obj = self.request.request(
1096+
"GET",
1097+
"home/feed/static",
1098+
base_url=self.config.api_v2_location,
1099+
params=params,
1100+
).json()
1101+
return self.page.parse(json_obj)
1102+
else:
1103+
return self.page.get("pages/home")
11001104

11011105
def explore(self) -> page.Page:
11021106
"""

tidalapi/user.py

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
def list_validate(lst):
5252
if isinstance(lst, str):
5353
lst = [lst]
54+
if isinstance(lst, int):
55+
lst = [str(lst)]
5456
if len(lst) == 0:
5557
raise ValueError("An empty list was provided.")
5658
return lst
@@ -555,7 +557,10 @@ def artists_paginated(
555557
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
556558
:return: A :class:`list` :class:`~tidalapi.artist.Artist` objects containing the favorite artists.
557559
"""
558-
return get_items(self.session.user.favorites.artists, order, order_direction)
560+
count = self.session.user.favorites.get_artists_count()
561+
return get_items(
562+
self.session.user.favorites.artists, count, order, order_direction
563+
)
559564

560565
def artists(
561566
self,
@@ -587,6 +592,21 @@ def artists(
587592
),
588593
)
589594

595+
def get_artists_count(
596+
self,
597+
) -> int:
598+
"""Get the total number of artists in the user's collection.
599+
600+
This performs a minimal API request (limit=1) to fetch metadata about the
601+
artists without retrieving all of them. The API response contains
602+
'totalNumberOfItems', which represents the total items (artists) available.
603+
:return: The number of items available.
604+
"""
605+
params = {"limit": 1, "offset": 0}
606+
607+
json_obj = self.requests.map_request(f"{self.base_url}/artists", params=params)
608+
return json_obj.get("totalNumberOfItems", 0)
609+
590610
def albums_paginated(
591611
self,
592612
order: Optional[AlbumOrder] = None,
@@ -598,7 +618,10 @@ def albums_paginated(
598618
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
599619
:return: A :class:`list` :class:`~tidalapi.album.Album` objects containing the favorite albums.
600620
"""
601-
return get_items(self.session.user.favorites.albums, order, order_direction)
621+
count = self.session.user.favorites.get_artists_count()
622+
return get_items(
623+
self.session.user.favorites.albums, count, order, order_direction
624+
)
602625

603626
def albums(
604627
self,
@@ -628,19 +651,36 @@ def albums(
628651
),
629652
)
630653

654+
def get_albums_count(
655+
self,
656+
) -> int:
657+
"""Get the total number of albums in the user's collection.
658+
659+
This performs a minimal API request (limit=1) to fetch metadata about the albums
660+
without retrieving all of them. The API response contains 'totalNumberOfItems',
661+
which represents the total items (albums) available.
662+
:return: The number of items available.
663+
"""
664+
params = {"limit": 1, "offset": 0}
665+
666+
json_obj = self.requests.map_request(f"{self.base_url}/albums", params=params)
667+
return json_obj.get("totalNumberOfItems", 0)
668+
631669
def playlists_paginated(
632670
self,
633671
order: Optional[PlaylistOrder] = None,
634672
order_direction: Optional[OrderDirection] = None,
635673
) -> List["Playlist"]:
636-
"""Get the users favorite playlists relative to the root folder, using
637-
pagination.
674+
"""Get the users favorite playlists, using pagination.
638675
639676
:param order: Optional; A :class:`PlaylistOrder` describing the ordering type when returning the user favorite playlists. eg.: "NAME, "DATE"
640677
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
641678
:return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite playlists.
642679
"""
643-
return get_items(self.session.user.favorites.playlists, order, order_direction)
680+
count = self.session.user.favorites.get_playlists_count()
681+
return get_items(
682+
self.session.user.favorites.playlists, count, order, order_direction
683+
)
644684

645685
def playlists(
646686
self,
@@ -666,10 +706,14 @@ def playlists(
666706
}
667707
if order:
668708
params["order"] = order.value
709+
else:
710+
params["order"] = PlaylistOrder.DateCreated.value
669711
if order_direction:
670712
params["orderDirection"] = order_direction.value
713+
else:
714+
params["orderDirection"] = OrderDirection.Descending.value
671715

672-
endpoint = "my-collection/playlists"
716+
endpoint = "my-collection/playlists/folders"
673717
return cast(
674718
List["Playlist"],
675719
self.session.request.map_request(
@@ -724,19 +768,41 @@ def playlist_folders(
724768
),
725769
)
726770

771+
def get_playlists_count(self) -> int:
772+
"""Get the total number of playlists in the user's root collection.
773+
774+
This performs a minimal API request (limit=1) to fetch metadata about the
775+
playlists without retrieving all of them. The API response contains
776+
'totalNumberOfItems', which represents the total playlists available.
777+
:return: The number of items available.
778+
"""
779+
params = {"folderId": "root", "offset": 0, "limit": 1, "includeOnly": ""}
780+
781+
endpoint = "my-collection/playlists/folders"
782+
json_obj = self.session.request.map_request(
783+
url=urljoin(
784+
self.session.config.api_v2_location,
785+
endpoint,
786+
),
787+
params=params,
788+
)
789+
return json_obj.get("totalNumberOfItems", 0)
790+
727791
def tracks_paginated(
728792
self,
729793
order: Optional[ItemOrder] = None,
730794
order_direction: Optional[OrderDirection] = None,
731795
) -> List["Playlist"]:
732-
"""Get the users favorite playlists relative to the root folder, using
733-
pagination.
796+
"""Get the users favorite tracks, using pagination.
734797
735798
:param order: Optional; A :class:`ItemOrder` describing the ordering type when returning the user favorite tracks. eg.: "NAME, "DATE"
736799
:param order_direction: Optional; A :class:`OrderDirection` describing the ordering direction when sorting by `order`. eg.: "ASC", "DESC"
737800
:return: A :class:`list` :class:`~tidalapi.playlist.Playlist` objects containing the favorite tracks.
738801
"""
739-
return get_items(self.session.user.favorites.tracks, order, order_direction)
802+
count = self.session.user.favorites.get_tracks_count()
803+
return get_items(
804+
self.session.user.favorites.tracks, count, order, order_direction
805+
)
740806

741807
def tracks(
742808
self,
@@ -766,6 +832,21 @@ def tracks(
766832
),
767833
)
768834

835+
def get_tracks_count(
836+
self,
837+
) -> int:
838+
"""Get the total number of tracks in the user's collection.
839+
840+
This performs a minimal API request (limit=1) to fetch metadata about the tracks
841+
without retrieving all of them. The API response contains 'totalNumberOfItems',
842+
which represents the total items (tracks) available.
843+
:return: The number of items available.
844+
"""
845+
params = {"limit": 1, "offset": 0}
846+
847+
json_obj = self.requests.map_request(f"{self.base_url}/tracks", params=params)
848+
return json_obj.get("totalNumberOfItems", 0)
849+
769850
def videos(
770851
self,
771852
limit: Optional[int] = None,

0 commit comments

Comments
 (0)