Skip to content

Commit 97b326a

Browse files
committed
Ensure all fields are parsed (_base_parse). Use a factory method for creating subclass instance, based on the type field.
1 parent 74874d9 commit 97b326a

File tree

1 file changed

+132
-65
lines changed

1 file changed

+132
-65
lines changed

tidalapi/page.py

Lines changed: 132 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ class Page:
7474
"""
7575

7676
title: str = ""
77-
categories: Optional[List["AllCategories"]] = None
78-
_categories_iter: Optional[Iterator["AllCategories"]] = None
77+
categories: Optional[List[Union["AllCategories", "AllCategoriesV2"]]] = None
78+
_categories_iter: Optional[Iterator[Union["AllCategories", "AllCategoriesV2"]]] = (
79+
None
80+
)
7981
_items_iter: Optional[Iterator[Callable[..., Any]]] = None
8082
page_category: "PageCategory"
8183
page_category_v2: "PageCategoryV2"
@@ -127,7 +129,7 @@ def parse(self, json_obj: JsonObj) -> "Page":
127129
self.categories.append(page_item)
128130
else:
129131
for item in json_obj["items"]:
130-
page_item = self.page_category_v2.parse(item)
132+
page_item = self.page_category_v2.parse_item(item)
131133
self.categories.append(page_item)
132134

133135
return copy.copy(self)
@@ -158,10 +160,13 @@ class More:
158160
@classmethod
159161
def parse(cls, json_obj: JsonObj) -> Optional["More"]:
160162
show_more = json_obj.get("showMore")
161-
if show_more is None:
162-
return None
163-
else:
163+
view_all = json_obj.get("viewAll")
164+
if show_more is not None:
164165
return cls(api_path=show_more["apiPath"], title=show_more["title"])
166+
elif view_all is not None:
167+
return cls(api_path=view_all, title=json_obj.get("title"))
168+
else:
169+
return None
165170

166171

167172
class PageCategory:
@@ -234,15 +239,33 @@ def show_more(self) -> Optional[Page]:
234239

235240

236241
class PageCategoryV2:
237-
type = None
242+
"""
243+
Base class for all V2 homepage page categories (e.g., TRACK_LIST, SHORTCUT_LIST).
244+
Handles shared fields and parsing logic, and automatically dispatches to the
245+
correct subclass based on the 'type' field in the JSON object.
246+
"""
247+
248+
# Registry mapping 'type' strings to subclass types
249+
_type_map: Dict[str, Type["PageCategoryV2"]] = {}
250+
251+
# Common metadata fields for all category types
252+
type: Optional[str] = None
253+
module_id: Optional[str] = None
238254
title: Optional[str] = None
255+
subtitle: Optional[str] = None
239256
description: Optional[str] = ""
240-
request: "Requests"
257+
_more: Optional["More"] = None
241258

242259
def __init__(self, session: "Session"):
260+
"""
261+
Store the shared session object and initialize common fields.
262+
Subclasses should implement their own `parse()` method but not override __init__.
263+
"""
243264
self.session = session
244265
self.request = session.request
245-
self.item_type_parser: Dict[str, Callable[..., Any]] = {
266+
267+
# Common item parsers by type (can be used by subclasses like SimpleList)
268+
self.item_types: Dict[str, Callable[..., Any]] = {
246269
"PLAYLIST": self.session.parse_playlist,
247270
"VIDEO": self.session.parse_video,
248271
"TRACK": self.session.parse_track,
@@ -251,59 +274,124 @@ def __init__(self, session: "Session"):
251274
"MIX": self.session.parse_v2_mix,
252275
}
253276

254-
def parse(self, json_obj: JsonObj) -> AllCategoriesV2:
255-
category_type = json_obj["type"]
256-
if category_type == "TRACK_LIST":
257-
category = TrackList(self.session)
258-
elif category_type == "SHORTCUT_LIST":
259-
category = ShortcutList(self.session)
260-
elif category_type == "HORIZONTAL_LIST":
261-
category = HorizontalList(self.session)
262-
elif category_type == "HORIZONTAL_LIST_WITH_CONTEXT":
263-
category = HorizontalListWithContext(self.session)
264-
else:
265-
raise NotImplementedError(f"PageType {category_type} not implemented")
277+
@classmethod
278+
def register_subclass(cls, category_type: str):
279+
"""
280+
Decorator to register subclasses in the _type_map.
281+
Usage:
282+
@PageCategoryV2.register_subclass("TRACK_LIST")
283+
class TrackList(PageCategoryV2):
284+
...
285+
"""
266286

267-
return category.parse(json_obj)
287+
def decorator(subclass):
288+
cls._type_map[category_type] = subclass
289+
subclass.category_type = category_type
290+
return subclass
268291

292+
return decorator
269293

270-
class SimpleList(PageCategoryV2):
271-
"""A simple list of different items for the home page V2."""
294+
def parse_item(self, list_item: Dict) -> "PageCategoryV2":
295+
"""
296+
Factory method that creates the correct subclass instance
297+
based on the 'type' field in item Dict, parses base fields,
298+
and then calls subclass parse().
299+
"""
300+
category_type = list_item.get("type")
301+
cls = self._type_map.get(category_type)
302+
if cls is None:
303+
raise NotImplementedError(f"Category {category_type} not implemented")
304+
instance = cls(self.session)
305+
instance._parse_base(list_item)
306+
instance.parse(list_item)
307+
return instance
308+
309+
def _parse_base(self, list_item: Dict):
310+
"""
311+
Parse fields common to all categories.
312+
"""
313+
self.type = list_item.get("type")
314+
self.module_id = list_item.get("moduleId")
315+
self.title = list_item.get("title")
316+
self.subtitle = list_item.get("subtitle")
317+
self.description = list_item.get("description")
318+
self._more = More.parse(list_item)
319+
320+
def parse(self, json_obj: JsonObj):
321+
"""
322+
Subclasses implement this method to parse category-specific data.
323+
"""
324+
raise NotImplementedError("Subclasses must implement parse()")
272325

273-
items: Optional[List[Any]] = None
326+
def view_all(self) -> Optional[Page]:
327+
"""View all items in a Get the full list of items on their own :class:`.Page` from a
328+
:class:`.PageCategory`
329+
330+
:return: A :class:`.Page` more of the items in the category, None if there aren't any
331+
"""
332+
api_path = self._more.api_path if self._more else None
333+
return self.session.view_all(api_path) if api_path and self._more else None
334+
335+
336+
class SimpleList(PageCategoryV2):
337+
"""
338+
A generic list of items (tracks, albums, playlists, etc.)
339+
using the shared self.item_types parser dictionary.
340+
"""
274341

275342
def __init__(self, session: "Session"):
276343
super().__init__(session)
277-
self.session = session
344+
self.items: List[Any] = []
278345

279-
def parse(self, json_obj: JsonObj) -> "SimpleList":
280-
self.items = []
281-
self.title = json_obj["title"]
346+
def parse(self, json_obj: "JsonObj"):
347+
self.items = [self.get_item(item) for item in json_obj["items"]]
348+
return self
282349

283-
for item in json_obj["items"]:
284-
self.items.append(self.get_item(item))
350+
def get_item(self, json_obj: "JsonObj") -> Any:
351+
item_type = json_obj.get("type")
352+
if item_type not in self.item_types:
353+
raise NotImplementedError(f"Item type '{item_type}' not implemented")
285354

286-
return self
355+
return self.item_types[item_type](json_obj["data"])
287356

288-
def get_item(self, json_obj):
289-
item_type = json_obj["type"]
290357

291-
try:
292-
if item_type in self.item_type_parser.keys():
293-
return self.item_type_parser[item_type](json_obj["data"])
294-
else:
295-
raise NotImplementedError(f"PageItemType {item_type} not implemented")
296-
except TypeError as e:
297-
print(f"Exception {e} while parsing SimpleList object.")
358+
@PageCategoryV2.register_subclass("SHORTCUT_LIST")
359+
class ShortcutList(SimpleList):
360+
"""
361+
A list of "shortcut" links (typically small horizontally scrollable rows).
362+
"""
298363

299364

300-
class HorizontalList(SimpleList): ...
365+
@PageCategoryV2.register_subclass("HORIZONTAL_LIST")
366+
class HorizontalList(SimpleList):
367+
"""
368+
A horizontal scrollable row of items.
369+
"""
301370

302371

303-
class HorizontalListWithContext(HorizontalList): ...
372+
@PageCategoryV2.register_subclass("HORIZONTAL_LIST_WITH_CONTEXT")
373+
class HorizontalListWithContext(HorizontalList):
374+
"""
375+
A horizontal list of items with additional context
376+
"""
377+
378+
379+
@PageCategoryV2.register_subclass("TRACK_LIST")
380+
class TrackList(PageCategoryV2):
381+
"""
382+
A category that represents a list of tracks, each one parsed with parse_track().
383+
"""
384+
385+
def __init__(self, session: "Session"):
386+
super().__init__(session)
387+
self.items: List[Any] = []
304388

389+
def parse(self, json_obj: "JsonObj"):
390+
self.items = [
391+
self.session.parse_track(item["data"]) for item in json_obj["items"]
392+
]
305393

306-
class ShortcutList(SimpleList): ...
394+
return self
307395

308396

309397
class FeaturedItems(PageCategory):
@@ -384,27 +472,6 @@ def parse(self, json_obj: JsonObj) -> "ItemList":
384472
return copy.copy(self)
385473

386474

387-
class TrackList(PageCategory):
388-
"""A list of tracks from TIDAL."""
389-
390-
items: Optional[List[Any]] = None
391-
392-
def parse(self, json_obj: JsonObj) -> "TrackList":
393-
"""Parse a list of tracks on TIDAL from the pages endpoints.
394-
395-
:param json_obj: The json from TIDAL to be parsed
396-
:return: A copy of the TrackList with a list of items
397-
"""
398-
self.title = json_obj["title"]
399-
400-
self.items = []
401-
402-
for item in json_obj["items"]:
403-
self.items.append(self.session.parse_track(item["data"]))
404-
405-
return copy.copy(self)
406-
407-
408475
class PageLink:
409476
"""A Link to another :class:`.Page` on TIDAL, Call get() to retrieve the Page."""
410477

0 commit comments

Comments
 (0)