@@ -74,8 +74,10 @@ class Page:
74
74
"""
75
75
76
76
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
+ )
79
81
_items_iter : Optional [Iterator [Callable [..., Any ]]] = None
80
82
page_category : "PageCategory"
81
83
page_category_v2 : "PageCategoryV2"
@@ -127,7 +129,7 @@ def parse(self, json_obj: JsonObj) -> "Page":
127
129
self .categories .append (page_item )
128
130
else :
129
131
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 )
131
133
self .categories .append (page_item )
132
134
133
135
return copy .copy (self )
@@ -158,10 +160,13 @@ class More:
158
160
@classmethod
159
161
def parse (cls , json_obj : JsonObj ) -> Optional ["More" ]:
160
162
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 :
164
165
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
165
170
166
171
167
172
class PageCategory :
@@ -234,15 +239,33 @@ def show_more(self) -> Optional[Page]:
234
239
235
240
236
241
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
238
254
title : Optional [str ] = None
255
+ subtitle : Optional [str ] = None
239
256
description : Optional [str ] = ""
240
- request : "Requests"
257
+ _more : Optional [ "More" ] = None
241
258
242
259
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
+ """
243
264
self .session = session
244
265
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 ]] = {
246
269
"PLAYLIST" : self .session .parse_playlist ,
247
270
"VIDEO" : self .session .parse_video ,
248
271
"TRACK" : self .session .parse_track ,
@@ -251,59 +274,124 @@ def __init__(self, session: "Session"):
251
274
"MIX" : self .session .parse_v2_mix ,
252
275
}
253
276
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
+ """
266
286
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
268
291
292
+ return decorator
269
293
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()" )
272
325
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
+ """
274
341
275
342
def __init__ (self , session : "Session" ):
276
343
super ().__init__ (session )
277
- self .session = session
344
+ self .items : List [ Any ] = []
278
345
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
282
349
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" )
285
354
286
- return self
355
+ return self . item_types [ item_type ]( json_obj [ "data" ])
287
356
288
- def get_item (self , json_obj ):
289
- item_type = json_obj ["type" ]
290
357
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
+ """
298
363
299
364
300
- class HorizontalList (SimpleList ): ...
365
+ @PageCategoryV2 .register_subclass ("HORIZONTAL_LIST" )
366
+ class HorizontalList (SimpleList ):
367
+ """
368
+ A horizontal scrollable row of items.
369
+ """
301
370
302
371
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 ] = []
304
388
389
+ def parse (self , json_obj : "JsonObj" ):
390
+ self .items = [
391
+ self .session .parse_track (item ["data" ]) for item in json_obj ["items" ]
392
+ ]
305
393
306
- class ShortcutList ( SimpleList ): ...
394
+ return self
307
395
308
396
309
397
class FeaturedItems (PageCategory ):
@@ -384,27 +472,6 @@ def parse(self, json_obj: JsonObj) -> "ItemList":
384
472
return copy .copy (self )
385
473
386
474
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
-
408
475
class PageLink :
409
476
"""A Link to another :class:`.Page` on TIDAL, Call get() to retrieve the Page."""
410
477
0 commit comments