1414from tenacity import retry , stop_after_attempt , wait_fixed
1515from ua_parser import user_agent_parser # type: ignore
1616
17- from sxm .models import (
18- LIVE_PRIMARY_HLS ,
19- QualitySize ,
20- RegionChoice ,
21- XMChannel ,
22- XMLiveChannel ,
23- )
17+ from sxm .models import QualitySize , RegionChoice , XMChannel , XMLiveChannel
2418
2519__all__ = [
2620 "HLS_AES_KEY" ,
4135REST_V4_FORMAT = "https://player.siriusxm.com/rest/v4/experience/modules/{}"
4236SESSION_MAX_LIFE = 14400
4337
44- ENABLE_NEW_CHANNELS = False
38+ ENABLE_NEW_CHANNELS = True
39+
40+
41+ class SXMError (Exception ):
42+ """Base class for all other SXM Errors"""
4543
4644
47- class AuthenticationError (Exception ):
45+ class ConfigurationError (SXMError ):
46+ """SXM Configuration retrive failed, renew session, and try again later"""
47+
48+
49+ class AuthenticationError (SXMError ):
4850 """SXM Authentication failed, renew session"""
4951
5052 pass
5153
5254
53- class SegmentRetrievalException (Exception ):
55+ class SegmentRetrievalException (SXMError ):
5456 """failed to get HLS segment, renew session"""
5557
5658 pass
@@ -106,8 +108,11 @@ class SXMClientAsync:
106108 _channels : Optional [List [XMChannel ]]
107109 _favorite_channels : Optional [List [XMChannel ]]
108110 _playlists : Dict [str , str ]
111+ _use_primary : bool
109112 _ua : Dict [str , Any ]
110113 _session : httpx .AsyncClient
114+ _configuration : Optional [Dict ] = None
115+ _urls : Optional [Dict [str , str ]] = None
111116
112117 def __init__ (
113118 self ,
@@ -140,6 +145,7 @@ def __init__(
140145 self ._playlists = {}
141146 self ._channels = None
142147 self ._favorite_channels = None
148+ self ._use_primary = True
143149
144150 # vars to manage session cache
145151 self .last_renew = None
@@ -148,6 +154,9 @@ def __init__(
148154 # hook function to call whenever the playlist updates
149155 self .update_handler = update_handler
150156
157+ def __del__ (self ):
158+ make_sync (self .close_session )()
159+
151160 @property
152161 def is_logged_in (self ) -> bool :
153162 return "SXMAUTHNEW" in self ._session .cookies
@@ -198,6 +207,63 @@ async def favorite_channels(self) -> List[XMChannel]:
198207 self ._favorite_channels = [c for c in await self .channels if c .is_favorite ]
199208 return self ._favorite_channels
200209
210+ def _extract_configuration (self , data : dict ):
211+ _config = {}
212+ config = data ["moduleList" ]["modules" ][0 ]["moduleResponse" ]["configuration" ][
213+ "components"
214+ ]
215+ for item in config :
216+ _config [item ["name" ]] = item
217+ return _config
218+
219+ @property
220+ async def configuration (self ) -> dict :
221+ if self ._configuration is None :
222+ data = await self .get_configuration ()
223+ if data is None :
224+ raise ConfigurationError ()
225+ self ._configuration = self ._extract_configuration (data )
226+
227+ return self ._configuration
228+
229+ def _extract_urls (self , urls : dict ):
230+ _urls = {}
231+ for url in urls ["settings" ][0 ]["relativeUrls" ]:
232+ if "url" in url :
233+ _urls [url ["name" ]] = url ["url" ]
234+
235+ return _urls
236+
237+ @property
238+ async def urls (self ) -> Dict [str , str ]:
239+ if self ._urls is None :
240+ urls = (await self .configuration )["relativeUrls" ]
241+ self ._urls = self ._extract_urls (urls )
242+ return self ._urls
243+
244+ @property
245+ def primary (self ) -> bool :
246+ return self ._use_primary
247+
248+ async def get_primary_hls_root (self ) -> str :
249+ urls = await self .urls
250+
251+ return urls ["Live_Primary_HLS" ]
252+
253+ async def get_secondary_hls_root (self ) -> str :
254+ urls = await self .urls
255+
256+ return urls ["Live_Secondary_HLS" ]
257+
258+ async def get_hls_root (self ) -> str :
259+ if self ._use_primary :
260+ return await self .get_primary_hls_root ()
261+ return await self .get_secondary_hls_root ()
262+
263+ def set_primary (self , value : bool ):
264+ self ._use_primary = value
265+ self ._playlists = {}
266+
201267 async def login (self ) -> bool :
202268 """Attempts to log into SXM with stored username/password"""
203269
@@ -251,6 +317,16 @@ async def authenticate(self) -> bool:
251317 self ._log .error (traceback .format_exc ())
252318 return False
253319
320+ @retry (wait = wait_fixed (3 ), stop = stop_after_attempt (10 ))
321+ async def get_configuration (self ) -> Optional [Dict [str , Any ]]:
322+ params = {
323+ "result-template" : "html5" ,
324+ "app-region" : self .region .value ,
325+ "cacheBuster" : str (int (time .time ())),
326+ }
327+
328+ return await self ._get ("get/configuration" , params = params )
329+
254330 @retry (stop = stop_after_attempt (25 ), wait = wait_fixed (1 ))
255331 async def get_playlist (
256332 self , channel_id : str , use_cache : bool = True
@@ -303,15 +379,13 @@ async def get_playlist(
303379 return "\n " .join (playlist_entries )
304380
305381 @retry (wait = wait_fixed (1 ), stop = stop_after_attempt (5 ))
306- async def get_segment (self , path : str , max_attempts : int = 5 ) -> Union [bytes , None ]:
382+ async def get_segment (self , path : str ) -> Union [bytes , None ]:
307383 """Gets raw HLS segment for given path
308384
309385 Parameters
310386 ----------
311387 path : :class:`str`
312388 SXM path
313- max_attempts : :class:`int`
314- Number of times to try to get segment. Defaults to 5.
315389
316390 Raises
317391 ------
@@ -320,7 +394,7 @@ async def get_segment(self, path: str, max_attempts: int = 5) -> Union[bytes, No
320394 needs reset
321395 """
322396
323- url = f" { LIVE_PRIMARY_HLS } / { path } "
397+ url = urllib . parse . urljoin ( await self . get_hls_root (), path )
324398 res = await self ._session .get (url , params = self ._token_params ())
325399
326400 if res .status_code == 403 :
@@ -436,6 +510,8 @@ def reset_session(self) -> None:
436510 self ._session_start = time .monotonic ()
437511 self ._session = httpx .AsyncClient ()
438512 self ._session .headers .update ({"User-Agent" : self ._ua ["string" ]})
513+ self ._urls = None
514+ self ._configuration = None
439515
440516 def _token_params (self ) -> Dict [str , Union [str , None ]]:
441517 return {
@@ -580,7 +656,10 @@ async def _post(
580656 )
581657
582658 async def _get_playlist_url (
583- self , channel_id : str , use_cache : bool = True , max_attempts : int = 5
659+ self ,
660+ channel_id : str ,
661+ use_cache : bool = True ,
662+ max_attempts : int = 5 ,
584663 ) -> Union [str , None ]:
585664 """Returns HLS live stream URL for a given `XMChannel`"""
586665
@@ -590,7 +669,6 @@ async def _get_playlist_url(
590669 return None
591670
592671 now = time .monotonic ()
593-
594672 if use_cache and channel .id in self ._playlists :
595673 if (
596674 self .last_renew is None
@@ -649,11 +727,18 @@ async def _get_playlist_url(
649727 live_channel_raw = data ["moduleList" ]["modules" ][0 ]
650728 live_channel = XMLiveChannel .from_dict (live_channel_raw )
651729 live_channel .set_stream_quality (self .stream_quality )
730+ live_channel .set_hls_roots (
731+ await self .get_primary_hls_root (), await self .get_secondary_hls_root ()
732+ )
652733
653734 self .update_interval = int (data ["moduleList" ]["modules" ][0 ]["updateFrequency" ])
654735
655736 # get m3u8 url
656- playlist = await self ._get_playlist_variant_url (live_channel .primary_hls .url )
737+ url = live_channel .primary_hls .url
738+ if not self ._use_primary :
739+ url = live_channel .secondary_hls .url
740+
741+ playlist = await self ._get_playlist_variant_url (url )
657742 if playlist is not None :
658743 self ._playlists [channel .id ] = playlist
659744 self .last_renew = time .monotonic ()
@@ -763,6 +848,10 @@ def update_interval(self) -> int:
763848 def username (self ) -> str :
764849 return self .async_client .username
765850
851+ @property
852+ def stream_quality (self ) -> QualitySize :
853+ return self .async_client .stream_quality
854+
766855 @property
767856 def is_logged_in (self ) -> bool :
768857 return self .async_client .is_logged_in
@@ -807,21 +896,57 @@ def favorite_channels(self) -> List[XMChannel]:
807896 ]
808897 return self .async_client ._favorite_channels
809898
899+ @property
900+ def configuration (self ) -> dict :
901+ if self .async_client ._configuration is None :
902+ data = self .get_configuration ()
903+ if data is None :
904+ raise ConfigurationError ()
905+
906+ self .async_client ._configuration = self .async_client ._extract_configuration (
907+ data
908+ )
909+ return self .async_client ._configuration
910+
911+ @property
912+ def urls (self ) -> Dict [str , str ]:
913+ if self .async_client ._urls is None :
914+ urls = self .configuration ["relativeUrls" ]
915+ self .async_client ._urls = self .async_client ._extract_urls (urls )
916+ return self .async_client ._urls
917+
918+ @property
919+ def primary (self ) -> bool :
920+ return self .async_client ._use_primary
921+
922+ def get_primary_hls_root (self ) -> str :
923+ return make_sync (self .async_client .get_primary_hls_root )()
924+
925+ def get_secondary_hls_root (self ) -> str :
926+ return make_sync (self .async_client .get_secondary_hls_root )()
927+
928+ def get_hls_root (self ) -> str :
929+ return make_sync (self .async_client .get_hls_root )()
930+
931+ def set_primary (self , value : bool ):
932+ self .async_client .set_primary (value )
933+
810934 def login (self ) -> bool :
811- return make_sync (self .async_client .is_logged_in )()
935+ return make_sync (self .async_client .login )()
812936
813937 def authenticate (self ) -> bool :
814938 return make_sync (self .async_client .authenticate )()
815939
940+ def get_configuration (self ) -> Optional [Dict [str , Any ]]:
941+ return make_sync (self .async_client .get_configuration )()
942+
816943 def get_playlist (self , channel_id : str , use_cache : bool = True ) -> Union [str , None ]:
817944 return make_sync (self .async_client .get_playlist )(
818945 channel_id = channel_id , use_cache = use_cache
819946 )
820947
821- def get_segment (self , path : str , max_attempts : int = 5 ) -> Union [bytes , None ]:
822- return make_sync (self .async_client .get_segment )(
823- path = path , max_attempts = max_attempts
824- )
948+ def get_segment (self , path : str ) -> Union [bytes , None ]:
949+ return make_sync (self .async_client .get_segment )(path = path )
825950
826951 def get_channels (self ) -> List [dict ]:
827952 return make_sync (self .async_client .get_channels )()
0 commit comments