diff --git a/VERSION b/VERSION index 1809198..47b322c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 +3.4.1 diff --git a/provider/api_client.py b/provider/api_client.py index 52365ac..6e6d94b 100644 --- a/provider/api_client.py +++ b/provider/api_client.py @@ -384,6 +384,12 @@ async def _do(c: ClientAsync) -> dict[str, Any] | None: runner = self._call_with_retry if with_retry else self._call_no_retry try: return await runner(_do) + except UnauthorizedError as err: + # Expired/invalidated token. Surface as LoginFailed so MA prompts + # for re-auth instead of the raw yandex_music exception bubbling + # through browse / play and crashing the caller. + LOGGER.warning("Rotor session POST %s: token no longer valid", path) + raise LoginFailed("Invalid Yandex Music token") from err except (NetworkError, ProviderUnavailableError) as err: LOGGER.warning("Rotor session POST %s failed: %s", path, err) return None diff --git a/tests/test_api_client.py b/tests/test_api_client.py index e260f24..92379cb 100644 --- a/tests/test_api_client.py +++ b/tests/test_api_client.py @@ -11,9 +11,9 @@ from unittest import mock import pytest -from music_assistant_models.errors import ResourceTemporarilyUnavailable +from music_assistant_models.errors import LoginFailed, ResourceTemporarilyUnavailable from ya_passport_auth import SecretStr -from yandex_music.exceptions import NetworkError +from yandex_music.exceptions import NetworkError, UnauthorizedError from yandex_music.rotor.dashboard import Dashboard from yandex_music.rotor.station_result import StationResult from yandex_music.utils.sign_request import DEFAULT_SIGN_KEY @@ -420,6 +420,24 @@ async def test_rotor_session_feedback_like_uses_trackid_without_seconds() -> Non assert "totalPlayedSeconds" not in event +async def test_rotor_session_request_maps_unauthorized_to_login_failed() -> None: + """Expired/invalid token during /rotor/session/* surfaces as LoginFailed. + + Without this mapping the raw ``UnauthorizedError`` from the MarshalX + client would bubble up through browse / play paths and crash the + provider instead of triggering MA's re-auth prompt. + """ + client, underlying = _make_client() + # _do is awaited via _call_with_retry → _ensure_connected → returns our + # AsyncMock underlying client. The underlying client's ._request.post is + # what actually raises. + underlying._request = mock.MagicMock() + underlying._request.post = mock.AsyncMock(side_effect=UnauthorizedError("stale token")) + + with pytest.raises(LoginFailed): + await client._rotor_session_request("new", {"seeds": ["user:onyourwave"]}) + + # -- get_similar_artists ------------------------------------------------------