@@ -81,12 +81,21 @@ class LinkLogin:
81
81
verification_uri : str
82
82
#: The link the user has to visit, with the code already included
83
83
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
84
90
85
91
def __init__ (self , json : JsonObj ):
86
92
self .expires_in = int (json ["expiresIn" ])
87
93
self .user_code = str (json ["userCode" ])
88
94
self .verification_uri = str (json ["verificationUri" ])
89
95
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" ])
90
99
91
100
92
101
class Config :
@@ -600,14 +609,17 @@ def login_oauth_simple(self, fn_print: Callable[[str], None] = print) -> None:
600
609
def login_oauth (self ) -> Tuple [LinkLogin , concurrent .futures .Future [Any ]]:
601
610
"""Login to TIDAL with a remote link for limited input devices. The function
602
611
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.
604
613
605
614
: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`
607
617
:raises: TimeoutError: If the login takes too long
608
618
"""
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 )
611
623
612
624
def save_session_to_file (self , session_file : Path ):
613
625
# create a new session
@@ -644,7 +656,13 @@ def load_session_from_file(self, session_file: Path):
644
656
645
657
return self .load_oauth_session (** args )
646
658
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
+ """
648
666
url = "https://auth.tidal.com/v1/oauth2/device_authorization"
649
667
params = {"client_id" : self .config .client_id , "scope" : "r_usr w_usr w_sub" }
650
668
@@ -655,24 +673,40 @@ def _login_with_link(self) -> Tuple[LinkLogin, concurrent.futures.Future[Any]]:
655
673
request .raise_for_status ()
656
674
657
675
json = request .json ()
658
- executor = concurrent .futures .ThreadPoolExecutor ()
659
- return LinkLogin (json ), executor .submit (self ._process_link_login , json )
660
676
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
664
697
665
698
def process_auth_token (
666
699
self , json : dict [str , Union [str , int ]], is_pkce_token : bool = True
667
- ) -> None :
700
+ ) -> bool :
668
701
"""Parses the authorization response and sets the token values to the specific
669
702
variables for further usage.
670
703
671
704
:param json: Parsed JSON response after login / authorization.
672
705
:type json: dict[str, str | int]
673
706
:param is_pkce_token: Set true if current token is obtained using PKCE
674
707
:type is_pkce_token: bool
675
- :return: None
708
+ :return: `True` if no error occurs.
709
+ :rtype: bool
676
710
"""
677
711
self .access_token = json ["access_token" ]
678
712
self .expiry_time = datetime .datetime .utcnow () + datetime .timedelta (
@@ -687,28 +721,45 @@ def process_auth_token(
687
721
self .user = user .User (self , user_id = json ["userId" ]).factory ()
688
722
self .is_pkce = is_pkce_token
689
723
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
694
741
url = self .config .api_oauth2_token
695
742
params = {
696
743
"client_id" : self .config .client_id ,
697
744
"client_secret" : self .config .client_secret ,
698
- "device_code" : device_code ,
745
+ "device_code" : link_login . device_code ,
699
746
"grant_type" : "urn:ietf:params:oauth:grant-type:device_code" ,
700
747
"scope" : "r_usr w_usr w_sub" ,
701
748
}
749
+
702
750
while expiry > 0 :
703
751
request = self .request_session .post (url , params )
704
- json = request .json ()
752
+ result : JsonObj = request .json ()
753
+
705
754
if request .ok :
706
- return json
755
+ return result
756
+
707
757
# 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" :
709
759
break
710
- time .sleep (interval )
711
- expiry = expiry - interval
760
+
761
+ time .sleep (link_login .interval )
762
+ expiry = expiry - link_login .interval
712
763
713
764
raise TimeoutError ("You took too long to log in" )
714
765
0 commit comments