Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.4.0
3.4.1
6 changes: 6 additions & 0 deletions provider/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 20 additions & 2 deletions tests/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ------------------------------------------------------


Expand Down
Loading