From 8cbedaf0a7d1e3e80974ccd94a48102bc95cae43 Mon Sep 17 00:00:00 2001 From: Nokse Date: Sat, 2 Aug 2025 11:29:21 +0200 Subject: [PATCH 1/5] Add x-tidal-client-version header --- tidalapi/request.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tidalapi/request.py b/tidalapi/request.py index e6bee54..26ecd6a 100644 --- a/tidalapi/request.py +++ b/tidalapi/request.py @@ -85,6 +85,8 @@ def basic_request( if not headers: headers = {} + headers["x-tidal-client-version"] = "2025.7.16" + if "User-Agent" not in headers: headers["User-Agent"] = self.user_agent From d209ab719c73d83781a2ccd43afc14c826672fa0 Mon Sep 17 00:00:00 2001 From: Nokse Date: Sat, 2 Aug 2025 12:33:41 +0200 Subject: [PATCH 2/5] Support new home page endpoint --- tidalapi/page.py | 62 +++++++++++++++++++++++++++++++++++++++++++++ tidalapi/session.py | 14 +++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/tidalapi/page.py b/tidalapi/page.py index 3696cf4..b8b0197 100644 --- a/tidalapi/page.py +++ b/tidalapi/page.py @@ -42,6 +42,8 @@ from tidalapi.request import Requests from tidalapi.session import Session +from . import album, artist, media, mix, playlist + PageCategories = Union[ "Album", "PageLinks", @@ -114,6 +116,17 @@ def parse(self, json_obj: JsonObj) -> "Page": return copy.copy(self) + def parseV2(self, json_obj: JsonObj) -> "Page": + """Goes through everything in the page, and gets the title and adds all the rows + to the categories field :param json_obj: The json to be parsed :return: A copy + of the Page that you can use to browse all the items.""" + self.categories = [] + for item in json_obj["items"]: + page_item = self.page_category.parse(item) + self.categories.append(page_item) + + return copy.copy(self) + def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> "Page": """Retrieve a page from the specified endpoint, overwrites the calling page. @@ -196,6 +209,10 @@ def parse(self, json_obj: JsonObj) -> AllCategories: elif category_type == "SOCIAL": json_obj["items"] = json_obj["socialProfiles"] category = LinkList(self.session) + elif category_type == "SHORTCUT_LIST": + category = ShortcutList(self.session) + elif category_type == "HORIZONTAL_LIST": + category = HorizontalList(self.session) else: raise NotImplementedError(f"PageType {category_type} not implemented") @@ -215,6 +232,51 @@ def show_more(self) -> Optional[Page]: ) +class SimpleList(PageCategory): + """A simple list of different items for the home page V2""" + + items: Optional[List[Any]] = None + + def __init__(self, session: "Session"): + super().__init__(session) + self.session = session + + def parse(self, json_obj: JsonObj) -> "SimpleList": + self.items = [] + self.title = json_obj["title"] + + for item in json_obj["items"]: + self.items.append(self.get_item(item)) + + return self + + def get_item(self, json_obj): + item_type = json_obj["type"] + item_data = json_obj["data"] + + if item_type == "PLAYLIST": + return self.session.parse_playlist(item_data) + elif item_type == "VIDEO": + return self.session.parse_video(item_data) + elif item_type == "TRACK": + return self.session.parse_track(item_data) + elif item_type == "ARTIST": + return self.session.parse_artist(item_data) + elif item_type == "ALBUM": + return self.session.parse_album(item_data) + elif item_type == "MIX": + return self.session.parse_mix(item_data) + raise NotImplementedError + + +class HorizontalList(SimpleList): + ... + + +class ShortcutList(SimpleList): + ... + + class FeaturedItems(PageCategory): """Items that have been featured by TIDAL.""" diff --git a/tidalapi/session.py b/tidalapi/session.py index 32b07b7..4673f86 100644 --- a/tidalapi/session.py +++ b/tidalapi/session.py @@ -1094,7 +1094,19 @@ def home(self) -> page.Page: :return: A :class:`.Page` object with the :class:`.PageCategory` list from the home page """ - return self.page.get("pages/home") + params = {} + params["deviceType"] = "BROWSER" + params["countryCode"] = "IT" + params["locale"] = "en_US" + params["platform"] = "WEB" + + json_obj = self.request.request( + "GET", + "home/feed/static", + base_url=self.config.api_v2_location, + params=params, + ).json() + return self.page.parseV2(json_obj) def explore(self) -> page.Page: """ From 989d63d16112e10bd046be3b86f6b8a3628cb9cc Mon Sep 17 00:00:00 2001 From: Nokse Date: Sat, 2 Aug 2025 13:58:28 +0200 Subject: [PATCH 3/5] Hack it to make it work --- tidalapi/page.py | 101 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/tidalapi/page.py b/tidalapi/page.py index b8b0197..bf52e3b 100644 --- a/tidalapi/page.py +++ b/tidalapi/page.py @@ -70,6 +70,7 @@ class Page: _categories_iter: Optional[Iterator["AllCategories"]] = None _items_iter: Optional[Iterator[Callable[..., Any]]] = None page_category: "PageCategory" + page_category_v2: "PageCategoryV2" request: "Requests" def __init__(self, session: "Session", title: str): @@ -77,6 +78,7 @@ def __init__(self, session: "Session", title: str): self.categories = None self.title = title self.page_category = PageCategory(session) + self.page_category_v2 = PageCategoryV2(session) def __iter__(self) -> "Page": if self.categories is None: @@ -117,12 +119,9 @@ def parse(self, json_obj: JsonObj) -> "Page": return copy.copy(self) def parseV2(self, json_obj: JsonObj) -> "Page": - """Goes through everything in the page, and gets the title and adds all the rows - to the categories field :param json_obj: The json to be parsed :return: A copy - of the Page that you can use to browse all the items.""" self.categories = [] for item in json_obj["items"]: - page_item = self.page_category.parse(item) + page_item = self.page_category_v2.parse(item) self.categories.append(page_item) return copy.copy(self) @@ -181,6 +180,7 @@ def __init__(self, session: "Session"): def parse(self, json_obj: JsonObj) -> AllCategories: result = None category_type = json_obj["type"] + print(category_type) if category_type in ("PAGE_LINKS_CLOUD", "PAGE_LINKS"): category: PageCategories = PageLinks(self.session) elif category_type in ("FEATURED_PROMOTIONS", "MULTIPLE_TOP_PROMOTIONS"): @@ -209,10 +209,6 @@ def parse(self, json_obj: JsonObj) -> AllCategories: elif category_type == "SOCIAL": json_obj["items"] = json_obj["socialProfiles"] category = LinkList(self.session) - elif category_type == "SHORTCUT_LIST": - category = ShortcutList(self.session) - elif category_type == "HORIZONTAL_LIST": - category = HorizontalList(self.session) else: raise NotImplementedError(f"PageType {category_type} not implemented") @@ -232,6 +228,41 @@ def show_more(self) -> Optional[Page]: ) +class PageCategoryV2: + type = None + title: Optional[str] = None + description: Optional[str] = "" + request: "Requests" + + def __init__(self, session: "Session"): + self.session = session + self.request = session.request + self.item_types: Dict[str, Callable[..., Any]] = { + "ALBUM_LIST": self.session.parse_album, + "ARTIST_LIST": self.session.parse_artist, + "TRACK_LIST": self.session.parse_track, + "PLAYLIST_LIST": self.session.parse_playlist, + "VIDEO_LIST": self.session.parse_video, + "MIX_LIST": self.session.parse_mix, + } + + def parse(self, json_obj: JsonObj) -> AllCategories: + category_type = json_obj["type"] + print(category_type) + # if category_type in self.item_types.keys(): + # category = ItemListV2(self.session) + # el + if category_type == "SHORTCUT_LIST": + category = ShortcutList(self.session) + elif category_type == "HORIZONTAL_LIST": + category = HorizontalList(self.session) + else: + return None + # raise NotImplementedError(f"PageType {category_type} not implemented") + + return category.parse(json_obj) + + class SimpleList(PageCategory): """A simple list of different items for the home page V2""" @@ -252,21 +283,22 @@ def parse(self, json_obj: JsonObj) -> "SimpleList": def get_item(self, json_obj): item_type = json_obj["type"] - item_data = json_obj["data"] + # item_data = json_obj["data"] if item_type == "PLAYLIST": - return self.session.parse_playlist(item_data) - elif item_type == "VIDEO": - return self.session.parse_video(item_data) - elif item_type == "TRACK": - return self.session.parse_track(item_data) - elif item_type == "ARTIST": - return self.session.parse_artist(item_data) - elif item_type == "ALBUM": - return self.session.parse_album(item_data) - elif item_type == "MIX": - return self.session.parse_mix(item_data) - raise NotImplementedError + return self.session.parse_playlist(json_obj) + # elif item_type == "VIDEO": + # return self.session.parse_video(item_data) + # elif item_type == "TRACK": + # return self.session.parse_track(item_data) + # elif item_type == "ARTIST": + # return self.session.parse_artist(item_data) + # elif item_type == "ALBUM": + # return self.session.parse_album(item_data) + # elif item_type == "MIX": + # return self.session.parse_v2_mix(json_obj) + # raise NotImplementedError + return None class HorizontalList(SimpleList): @@ -355,6 +387,33 @@ def parse(self, json_obj: JsonObj) -> "ItemList": return copy.copy(self) +class ItemListV2(PageCategory): + """A list of items from TIDAL, can be a list of mixes, for example, or a list of + playlists and mixes in some cases.""" + + items: Optional[List[Any]] = None + + def parse(self, json_obj: JsonObj) -> "ItemListV2": + """Parse a list of items on TIDAL from the pages endpoints. + + :param json_obj: The json from TIDAL to be parsed + :return: A copy of the ItemListV2 with a list of items + """ + self.title = json_obj["title"] + item_type = json_obj["type"] + session: Optional["Session"] = None + parse: Optional[Callable[..., Any]] = None + + if item_type in self.item_types.keys(): + parse = self.item_types[item_type] + else: + raise NotImplementedError("PageType {} not implemented".format(item_type)) + + self.items = self.request.map_json(json_obj["items"], parse, session) + + return copy.copy(self) + + class PageLink: """A Link to another :class:`.Page` on TIDAL, Call get() to retrieve the Page.""" From d661bd6cbac42e3e37635eaf1954b6618260a907 Mon Sep 17 00:00:00 2001 From: Nokse Date: Sat, 2 Aug 2025 16:07:10 +0200 Subject: [PATCH 4/5] Support other item types --- tidalapi/page.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tidalapi/page.py b/tidalapi/page.py index bf52e3b..a578f71 100644 --- a/tidalapi/page.py +++ b/tidalapi/page.py @@ -285,18 +285,23 @@ def get_item(self, json_obj): item_type = json_obj["type"] # item_data = json_obj["data"] - if item_type == "PLAYLIST": - return self.session.parse_playlist(json_obj) - # elif item_type == "VIDEO": - # return self.session.parse_video(item_data) - # elif item_type == "TRACK": - # return self.session.parse_track(item_data) - # elif item_type == "ARTIST": - # return self.session.parse_artist(item_data) - # elif item_type == "ALBUM": - # return self.session.parse_album(item_data) - # elif item_type == "MIX": - # return self.session.parse_v2_mix(json_obj) + print(item_type) + + try: + if item_type == "PLAYLIST": + return self.session.parse_playlist(json_obj) + elif item_type == "VIDEO": + return self.session.parse_video(json_obj["data"]) + elif item_type == "TRACK": + return self.session.parse_track(json_obj["data"]) + elif item_type == "ARTIST": + return self.session.parse_artist(json_obj["data"]) + elif item_type == "ALBUM": + return self.session.parse_album(json_obj["data"]) + elif item_type == "MIX": + return self.session.parse_mix(json_obj["data"]) + except Exception as e: + print(e) # raise NotImplementedError return None From 2423c127b24d38a953a24585c41d57fa0eb378be Mon Sep 17 00:00:00 2001 From: Nokse Date: Sat, 2 Aug 2025 16:27:58 +0200 Subject: [PATCH 5/5] Support TRACK_LIST --- tidalapi/page.py | 44 ++++++++++++++------------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/tidalapi/page.py b/tidalapi/page.py index a578f71..4efb71f 100644 --- a/tidalapi/page.py +++ b/tidalapi/page.py @@ -237,28 +237,18 @@ class PageCategoryV2: def __init__(self, session: "Session"): self.session = session self.request = session.request - self.item_types: Dict[str, Callable[..., Any]] = { - "ALBUM_LIST": self.session.parse_album, - "ARTIST_LIST": self.session.parse_artist, - "TRACK_LIST": self.session.parse_track, - "PLAYLIST_LIST": self.session.parse_playlist, - "VIDEO_LIST": self.session.parse_video, - "MIX_LIST": self.session.parse_mix, - } def parse(self, json_obj: JsonObj) -> AllCategories: category_type = json_obj["type"] print(category_type) - # if category_type in self.item_types.keys(): - # category = ItemListV2(self.session) - # el - if category_type == "SHORTCUT_LIST": + if category_type == "TRACK_LIST": + category = TrackList(self.session) + elif category_type == "SHORTCUT_LIST": category = ShortcutList(self.session) elif category_type == "HORIZONTAL_LIST": category = HorizontalList(self.session) else: - return None - # raise NotImplementedError(f"PageType {category_type} not implemented") + raise NotImplementedError(f"PageType {category_type} not implemented") return category.parse(json_obj) @@ -298,8 +288,8 @@ def get_item(self, json_obj): return self.session.parse_artist(json_obj["data"]) elif item_type == "ALBUM": return self.session.parse_album(json_obj["data"]) - elif item_type == "MIX": - return self.session.parse_mix(json_obj["data"]) + # elif item_type == "MIX": + # return self.session.mix(json_obj["data"]["id"]) except Exception as e: print(e) # raise NotImplementedError @@ -392,29 +382,23 @@ def parse(self, json_obj: JsonObj) -> "ItemList": return copy.copy(self) -class ItemListV2(PageCategory): - """A list of items from TIDAL, can be a list of mixes, for example, or a list of - playlists and mixes in some cases.""" +class TrackList(PageCategory): + """A list of track from TIDAL.""" items: Optional[List[Any]] = None - def parse(self, json_obj: JsonObj) -> "ItemListV2": - """Parse a list of items on TIDAL from the pages endpoints. + def parse(self, json_obj: JsonObj) -> "TrackList": + """Parse a list of tracks on TIDAL from the pages endpoints. :param json_obj: The json from TIDAL to be parsed - :return: A copy of the ItemListV2 with a list of items + :return: A copy of the TrackList with a list of items """ self.title = json_obj["title"] - item_type = json_obj["type"] - session: Optional["Session"] = None - parse: Optional[Callable[..., Any]] = None - if item_type in self.item_types.keys(): - parse = self.item_types[item_type] - else: - raise NotImplementedError("PageType {} not implemented".format(item_type)) + self.items = [] - self.items = self.request.map_json(json_obj["items"], parse, session) + for item in json_obj["items"]: + self.items.append(self.session.parse_track(item["data"])) return copy.copy(self)