diff --git a/HISTORY.rst b/HISTORY.rst index 70399b7..ff51645 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -21,6 +21,7 @@ v0.9.0 * Bugfix: Non-JSON error responses and `Retry-After` given as a date no longer break error handling - tehkillerbee_ * Bugfix: Parsing tracks and albums no longer fails when TIDAL returns no or empty artist information - tehkillerbee_ * Parsed `Track` flags (`pay_to_stream`, `premium_streaming_only`, `editable`, `upload`, `spotlighted`) are now `False` instead of `None` when TIDAL omits them, and `url` is an empty string - tehkillerbee_ +* Bugfix: `load_oauth_session` accepts the expiry time as an ISO 8601 string, as applications that store it as text pass it. Since the token is now refreshed before it expires, such a string made every request fail - Petru101_ v0.8.11 -------- @@ -323,4 +324,5 @@ v0.6.2 .. _nilathedragon: https://github.com/nilathedragon .. _semohr: https://github.com/semohr .. _rafrombrc: https://github.com/rafrombrc +.. _Petru101: https://github.com/Petru101 diff --git a/tests/test_token_refresh.py b/tests/test_token_refresh.py index d857e71..2e44162 100644 --- a/tests/test_token_refresh.py +++ b/tests/test_token_refresh.py @@ -196,6 +196,45 @@ def test_failed_proactive_refresh_does_not_fail_the_request(): assert session.request.basic_request("GET", "tracks/1").ok +def _load_oauth_session(expiry_time): + session = _refreshing_session(None) + session.request_session.request.return_value = fake_response( + 200, {"sessionId": "sid", "countryCode": "US", "userId": 1} + ) + with mock.patch.object(tidalapi.user.User, "factory"): + assert session.load_oauth_session( + "Bearer", "old-access", "old-refresh", expiry_time, is_pkce=True + ) + return session + + +def test_load_oauth_session_accepts_expiry_time_string(): + expired = datetime.datetime.utcnow() - datetime.timedelta(days=1) + + session = _load_oauth_session(str(expired)) + + # The expired token is refreshed up front, which sets a new expiry time + session.request_session.post.assert_called_once() + assert session.access_token == "new-access" + assert session.expiry_time > datetime.datetime.utcnow() + + +def test_load_oauth_session_accepts_iso_format_expiry_time(): + expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5) + + session = _load_oauth_session(expiry_time.isoformat()) + + assert session.expiry_time == expiry_time + session.request_session.post.assert_not_called() + + +def test_load_oauth_session_ignores_invalid_expiry_time_string(): + session = _load_oauth_session("None") + + assert session.expiry_time is None + session.request_session.post.assert_not_called() + + def test_session_file_round_trips_expiry_time(tmp_path): session_file = tmp_path / "session.json" expiry_time = datetime.datetime(2030, 1, 2, 3, 4, 5) diff --git a/tidalapi/session.py b/tidalapi/session.py index 2b364c0..8bfb243 100644 --- a/tidalapi/session.py +++ b/tidalapi/session.py @@ -388,7 +388,7 @@ def load_oauth_session( token_type: str, access_token: str, refresh_token: Optional[str] = None, - expiry_time: Optional[datetime.datetime] = None, + expiry_time: Optional[Union[datetime.datetime, str]] = None, is_pkce: Optional[bool] = False, ) -> bool: """Login to TIDAL using details from a previous OAuth login, automatically @@ -398,10 +398,19 @@ def load_oauth_session( :param access_token: The access token received from an oauth login or refresh :param refresh_token: (Optional) A refresh token that lets you get a new access token after it has expired - :param expiry_time: (Optional) The datetime the access token will expire + :param expiry_time: (Optional) The datetime the access token will expire. A + string in ISO 8601 format (e.g. ``str(datetime)``) is accepted too :param is_pkce: (Optional) Is session pkce? :return: True if we believe the login was successful, otherwise false. """ + if isinstance(expiry_time, str): + # Applications often store the expiry time as text + try: + expiry_time = datetime.datetime.fromisoformat(expiry_time) + except ValueError: + log.info("Ignoring invalid expiry_time %r", expiry_time) + expiry_time = None + self.token_type = token_type self.access_token = access_token self.refresh_token = refresh_token