@@ -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
167172class PageCategory :
@@ -234,15 +239,34 @@ def show_more(self) -> Optional[Page]:
234239
235240
236241class PageCategoryV2 :
237- type = None
242+ """Base class for all V2 homepage page categories (e.g., TRACK_LIST, SHORTCUT_LIST).
243+
244+ Handles shared fields and parsing logic, and automatically dispatches to the correct
245+ 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+ """Store the shared session object and initialize common fields.
261+
262+ Subclasses should implement their own `parse()` method but not override
263+ __init__.
264+ """
243265 self .session = session
244266 self .request = session .request
245- self .item_type_parser : Dict [str , Callable [..., Any ]] = {
267+
268+ # Common item parsers by type (can be used by subclasses like SimpleList)
269+ self .item_types : Dict [str , Callable [..., Any ]] = {
246270 "PLAYLIST" : self .session .parse_playlist ,
247271 "VIDEO" : self .session .parse_video ,
248272 "TRACK" : self .session .parse_track ,
@@ -251,59 +275,108 @@ def __init__(self, session: "Session"):
251275 "MIX" : self .session .parse_v2_mix ,
252276 }
253277
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" )
278+ @classmethod
279+ def register_subclass (cls , category_type : str ):
280+ """Decorator to register subclasses in the _type_map.
266281
267- return category .parse (json_obj )
282+ Usage:
283+ @PageCategoryV2.register_subclass("TRACK_LIST")
284+ class TrackList(PageCategoryV2):
285+ ...
286+ """
268287
288+ def decorator (subclass ):
289+ cls ._type_map [category_type ] = subclass
290+ subclass .category_type = category_type
291+ return subclass
292+
293+ return decorator
294+
295+ def parse_item (self , list_item : Dict ) -> "PageCategoryV2" :
296+ """Factory method that creates the correct subclass instance based on the 'type'
297+ field in item Dict, parses base fields, and then calls subclass parse()."""
298+ category_type = list_item .get ("type" )
299+ cls = self ._type_map .get (category_type )
300+ if cls is None :
301+ raise NotImplementedError (f"Category { category_type } not implemented" )
302+ instance = cls (self .session )
303+ instance ._parse_base (list_item )
304+ instance .parse (list_item )
305+ return instance
306+
307+ def _parse_base (self , list_item : Dict ):
308+ """Parse fields common to all categories."""
309+ self .type = list_item .get ("type" )
310+ self .module_id = list_item .get ("moduleId" )
311+ self .title = list_item .get ("title" )
312+ self .subtitle = list_item .get ("subtitle" )
313+ self .description = list_item .get ("description" )
314+ self ._more = More .parse (list_item )
315+
316+ def parse (self , json_obj : JsonObj ):
317+ """Subclasses implement this method to parse category-specific data."""
318+ raise NotImplementedError ("Subclasses must implement parse()" )
319+
320+ def view_all (self ) -> Optional [Page ]:
321+ """View all items in a Get the full list of items on their own :class:`.Page`
322+ from a :class:`.PageCategory`
269323
270- class SimpleList (PageCategoryV2 ):
271- """A simple list of different items for the home page V2."""
324+ :return: A :class:`.Page` more of the items in the category, None if there aren't any
325+ """
326+ api_path = self ._more .api_path if self ._more else None
327+ return self .session .view_all (api_path ) if api_path and self ._more else None
272328
273- items : Optional [List [Any ]] = None
329+
330+ class SimpleList (PageCategoryV2 ):
331+ """A generic list of items (tracks, albums, playlists, etc.) using the shared
332+ self.item_types parser dictionary."""
274333
275334 def __init__ (self , session : "Session" ):
276335 super ().__init__ (session )
277- self .session = session
336+ self .items : List [ Any ] = []
278337
279- def parse (self , json_obj : JsonObj ) -> "SimpleList" :
280- self .items = []
281- self . title = json_obj [ "title" ]
338+ def parse (self , json_obj : " JsonObj" ) :
339+ self .items = [self . get_item ( item ) for item in json_obj [ "items" ] ]
340+ return self
282341
283- for item in json_obj ["items" ]:
284- self .items .append (self .get_item (item ))
342+ def get_item (self , json_obj : "JsonObj" ) -> Any :
343+ item_type = json_obj .get ("type" )
344+ if item_type not in self .item_types :
345+ raise NotImplementedError (f"Item type '{ item_type } ' not implemented" )
285346
286- return self
347+ return self . item_types [ item_type ]( json_obj [ "data" ])
287348
288- def get_item (self , json_obj ):
289- item_type = json_obj ["type" ]
290349
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." )
350+ @PageCategoryV2 .register_subclass ("SHORTCUT_LIST" )
351+ class ShortcutList (SimpleList ):
352+ """A list of "shortcut" links (typically small horizontally scrollable rows)."""
298353
299354
300- class HorizontalList (SimpleList ): ...
355+ @PageCategoryV2 .register_subclass ("HORIZONTAL_LIST" )
356+ class HorizontalList (SimpleList ):
357+ """A horizontal scrollable row of items."""
301358
302359
303- class HorizontalListWithContext (HorizontalList ): ...
360+ @PageCategoryV2 .register_subclass ("HORIZONTAL_LIST_WITH_CONTEXT" )
361+ class HorizontalListWithContext (HorizontalList ):
362+ """A horizontal list of items with additional context."""
304363
305364
306- class ShortcutList (SimpleList ): ...
365+ @PageCategoryV2 .register_subclass ("TRACK_LIST" )
366+ class TrackList (PageCategoryV2 ):
367+ """A category that represents a list of tracks, each one parsed with
368+ parse_track()."""
369+
370+ def __init__ (self , session : "Session" ):
371+ super ().__init__ (session )
372+ self .items : List [Any ] = []
373+
374+ def parse (self , json_obj : "JsonObj" ):
375+ self .items = [
376+ self .session .parse_track (item ["data" ]) for item in json_obj ["items" ]
377+ ]
378+
379+ return self
307380
308381
309382class FeaturedItems (PageCategory ):
@@ -384,27 +457,6 @@ def parse(self, json_obj: JsonObj) -> "ItemList":
384457 return copy .copy (self )
385458
386459
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-
408460class PageLink :
409461 """A Link to another :class:`.Page` on TIDAL, Call get() to retrieve the Page."""
410462
0 commit comments