@@ -81,12 +81,21 @@ class LinkLogin:
8181 verification_uri : str
8282 #: The link the user has to visit, with the code already included
8383 verification_uri_complete : str
84+ #: After how much time the uri expires.
85+ expires_in : float
86+ #: The interval for authorization checks against the backend.
87+ interval : float
88+ #: The unique device code necessary for authorization.
89+ device_code : str
8490
8591 def __init__ (self , json : JsonObj ):
8692 self .expires_in = int (json ["expiresIn" ])
8793 self .user_code = str (json ["userCode" ])
8894 self .verification_uri = str (json ["verificationUri" ])
8995 self .verification_uri_complete = str (json ["verificationUriComplete" ])
96+ self .expires_in = float (json ["expiresIn" ])
97+ self .interval = float (json ["interval" ])
98+ self .device_code = str (json ["deviceCode" ])
9099
91100
92101class Config :
@@ -600,14 +609,17 @@ def login_oauth_simple(self, fn_print: Callable[[str], None] = print) -> None:
600609 def login_oauth (self ) -> Tuple [LinkLogin , concurrent .futures .Future [Any ]]:
601610 """Login to TIDAL with a remote link for limited input devices. The function
602611 will return everything you need to log in through a web browser, and will return
603- an future that will run until login.
612+ a future that will run until login.
604613
605614 :return: A :class:`LinkLogin` object containing all the data needed to log in remotely, and
606- a :class:`concurrent.futures.Future` that will poll until the login is completed, or until the link expires.
615+ a :class:`concurrent.futures.Future` object that will poll until the login is completed, or until the link expires.
616+ :rtype: :class:`LinkLogin`
607617 :raises: TimeoutError: If the login takes too long
608618 """
609- login , future = self ._login_with_link ()
610- return login , future
619+ link_login : LinkLogin = self .get_link_login ()
620+ executor = concurrent .futures .ThreadPoolExecutor ()
621+
622+ return link_login , executor .submit (self .process_link_login , link_login )
611623
612624 def save_session_to_file (self , session_file : Path ):
613625 # create a new session
@@ -644,7 +656,13 @@ def load_session_from_file(self, session_file: Path):
644656
645657 return self .load_oauth_session (** args )
646658
647- def _login_with_link (self ) -> Tuple [LinkLogin , concurrent .futures .Future [Any ]]:
659+ def get_link_login (self ) -> LinkLogin :
660+ """Return information required to login into TIDAL using a device authorization
661+ link.
662+
663+ :return: Login information for device authorization retrieved from the TIDAL backend.
664+ :rtype: :class:`LinkLogin`
665+ """
648666 url = "https://auth.tidal.com/v1/oauth2/device_authorization"
649667 params = {"client_id" : self .config .client_id , "scope" : "r_usr w_usr w_sub" }
650668
@@ -655,24 +673,40 @@ def _login_with_link(self) -> Tuple[LinkLogin, concurrent.futures.Future[Any]]:
655673 request .raise_for_status ()
656674
657675 json = request .json ()
658- executor = concurrent .futures .ThreadPoolExecutor ()
659- return LinkLogin (json ), executor .submit (self ._process_link_login , json )
660676
661- def _process_link_login (self , json : JsonObj ) -> None :
662- json = self ._wait_for_link_login (json )
663- self .process_auth_token (json , is_pkce_token = False )
677+ return LinkLogin (json )
678+
679+ def process_link_login (
680+ self , link_login : LinkLogin , until_expiry : bool = True
681+ ) -> bool :
682+ """Checks if device authorization was successful and processes the retrieved
683+ OAuth token from the Backend.
684+
685+ :param link_login: Link login information containing the necessary device authorization information.
686+ :type link_login: :class:`LinkLogin`
687+ :param until_expiry: If `True` device authorization check is running until the link expires. If `False`check is running only once.
688+ :type until_expiry: :class:`bool`
689+
690+ :return: `True` if login was successful.
691+ :rtype: bool
692+ """
693+ result : JsonObj = self ._check_link_login (link_login , until_expiry )
694+ result_process : bool = self .process_auth_token (result , is_pkce_token = False )
695+
696+ return result_process
664697
665698 def process_auth_token (
666699 self , json : dict [str , Union [str , int ]], is_pkce_token : bool = True
667- ) -> None :
700+ ) -> bool :
668701 """Parses the authorization response and sets the token values to the specific
669702 variables for further usage.
670703
671704 :param json: Parsed JSON response after login / authorization.
672705 :type json: dict[str, str | int]
673706 :param is_pkce_token: Set true if current token is obtained using PKCE
674707 :type is_pkce_token: bool
675- :return: None
708+ :return: `True` if no error occurs.
709+ :rtype: bool
676710 """
677711 self .access_token = json ["access_token" ]
678712 self .expiry_time = datetime .datetime .utcnow () + datetime .timedelta (
@@ -687,28 +721,45 @@ def process_auth_token(
687721 self .user = user .User (self , user_id = json ["userId" ]).factory ()
688722 self .is_pkce = is_pkce_token
689723
690- def _wait_for_link_login (self , json : JsonObj ) -> Any :
691- expiry = float (json ["expiresIn" ])
692- interval = float (json ["interval" ])
693- device_code = json ["deviceCode" ]
724+ return True
725+
726+ def _check_link_login (
727+ self , link_login : LinkLogin , until_expiry : bool = True
728+ ) -> TimeoutError | JsonObj :
729+ """Checks if device authorization was successful and retrieves OAuth data. Can
730+ check the backend for successful device authrization until the link expires
731+ (with the given interval) or just once.
732+
733+ :param link_login: Link login information containing the necessary device authorization information.
734+ :type link_login: :class:`LinkLogin`
735+ :param until_expiry: If `True` device authorization check is running until the link expires. If `False`check is running only once.
736+ :type until_expiry: :class:`bool`
737+ :return: Raise :class:`TimeoutError` if the link has expired otherwise returns retrieved OAuth information.
738+ :rtype: :class:`TimeoutError` | :class:`JsonObj`
739+ """
740+ expiry : float = link_login .expires_in if until_expiry else 1
694741 url = self .config .api_oauth2_token
695742 params = {
696743 "client_id" : self .config .client_id ,
697744 "client_secret" : self .config .client_secret ,
698- "device_code" : device_code ,
745+ "device_code" : link_login . device_code ,
699746 "grant_type" : "urn:ietf:params:oauth:grant-type:device_code" ,
700747 "scope" : "r_usr w_usr w_sub" ,
701748 }
749+
702750 while expiry > 0 :
703751 request = self .request_session .post (url , params )
704- json = request .json ()
752+ result : JsonObj = request .json ()
753+
705754 if request .ok :
706- return json
755+ return result
756+
707757 # Because the requests take time, the expiry variable won't be accurate, so stop if TIDAL says it's expired
708- if json ["error" ] == "expired_token" :
758+ if result ["error" ] == "expired_token" :
709759 break
710- time .sleep (interval )
711- expiry = expiry - interval
760+
761+ time .sleep (link_login .interval )
762+ expiry = expiry - link_login .interval
712763
713764 raise TimeoutError ("You took too long to log in" )
714765
0 commit comments