Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
92a6c86
feat(yandex_music): add QR authentication and token auto-refresh
trudenboy Apr 7, 2026
9eea38b
feat(yandex_music): sync provider from ma-provider-yandex-music v2.7.2
github-actions[bot] Apr 7, 2026
13d605d
feat(yandex_music): sync provider from ma-provider-yandex-music v2.7.2
github-actions[bot] Apr 7, 2026
8c03ad6
feat(yandex_music): sync provider from ma-provider-yandex-music v2.8.0
github-actions[bot] Apr 9, 2026
aeb818a
feat(yandex_music): sync provider from ma-provider-yandex-music v2.8.0
github-actions[bot] Apr 9, 2026
e66dbe7
feat(yandex_music): sync provider from ma-provider-yandex-music v2.8.0
github-actions[bot] Apr 9, 2026
bd16eba
feat(yandex_music): sync provider from ma-provider-yandex-music v2.8.0
github-actions[bot] Apr 9, 2026
6cb58db
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 9, 2026
097a956
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 9, 2026
5c89c3c
feat(yandex_music): sync provider from ma-provider-yandex-music v2.8.0
github-actions[bot] Apr 9, 2026
139a75f
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 10, 2026
cc48936
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 10, 2026
86cc63c
chore: regenerate requirements_all.txt
trudenboy Apr 10, 2026
d4160dd
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 11, 2026
7e33d1e
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 11, 2026
e84549b
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 13, 2026
4cbb976
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 13, 2026
cf5e8cc
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 15, 2026
2d72c82
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 15, 2026
8101c40
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 15, 2026
4343118
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 16, 2026
ae05b14
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 16, 2026
8d81717
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.0
github-actions[bot] Apr 16, 2026
ed76724
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 17, 2026
fb45738
feat(yandex_music): sync provider from ma-provider-yandex-music v2.9.1
github-actions[bot] Apr 17, 2026
431fe98
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.0
github-actions[bot] Apr 19, 2026
917ecc0
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.0
github-actions[bot] Apr 19, 2026
c98604d
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 19, 2026
496ee52
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 20, 2026
faebce6
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.0
github-actions[bot] Apr 20, 2026
f4c1642
feat(kion_music): sync provider from ma-provider-kion-music v2.6.7
github-actions[bot] Apr 20, 2026
7024760
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.0
github-actions[bot] Apr 20, 2026
d8deb2c
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 20, 2026
99f76c7
Remove kion_music provider from yandex_music PR branch
trudenboy Apr 20, 2026
66acb7b
Restore kion_music to upstream/dev state
trudenboy Apr 20, 2026
8ab1309
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.0
github-actions[bot] Apr 20, 2026
c28c7ad
Bump kion_music yandex-music requirement to 3.0.0
trudenboy Apr 20, 2026
3376fbf
Revert "Bump kion_music yandex-music requirement to 3.0.0"
trudenboy Apr 20, 2026
94015a7
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 20, 2026
0bb21a4
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.0
github-actions[bot] Apr 20, 2026
5577981
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 20, 2026
bd801d6
Update ya-passport-auth and yandex-music versions
trudenboy Apr 20, 2026
b566e91
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 20, 2026
ff3c031
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 20, 2026
d7b0644
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.1
github-actions[bot] Apr 20, 2026
e4187b9
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.1
github-actions[bot] Apr 20, 2026
d4df765
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 21, 2026
67f1d51
feat(yandex_music): sync provider from ma-provider-yandex-music v3.0.1
github-actions[bot] Apr 21, 2026
72e1d05
feat(yandex_music): sync provider from ma-provider-yandex-music v3.1.0
github-actions[bot] Apr 21, 2026
b798cd1
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 21, 2026
e64d861
feat(yandex_music): sync provider from ma-provider-yandex-music v3.1.1
github-actions[bot] Apr 21, 2026
ed104c1
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 21, 2026
3612ae7
feat(yandex_music): sync provider from ma-provider-yandex-music v3.1.2
github-actions[bot] Apr 21, 2026
826dc09
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 21, 2026
681e0ba
feat(yandex_music): sync provider from ma-provider-yandex-music v3.2.0
github-actions[bot] Apr 21, 2026
cd6cd2f
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 22, 2026
dc9b7cf
feat(yandex_music): sync provider from ma-provider-yandex-music v3.2.1
github-actions[bot] Apr 22, 2026
be93423
feat(yandex_music): sync provider from ma-provider-yandex-music v3.3.0
github-actions[bot] Apr 23, 2026
e5a70ce
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 23, 2026
c172f24
feat(yandex_music): sync provider from ma-provider-yandex-music v3.3.1
github-actions[bot] Apr 23, 2026
1fa8115
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 23, 2026
869ef78
feat(yandex_music): sync provider from ma-provider-yandex-music v3.4.0
github-actions[bot] Apr 23, 2026
db23b01
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 23, 2026
3b439fd
feat(yandex_music): sync provider from ma-provider-yandex-music v3.4.0
github-actions[bot] Apr 23, 2026
9614f95
Remove gql dependency from requirements
trudenboy Apr 23, 2026
be4d0f9
Update requirements_all.txt to remove warning
trudenboy Apr 23, 2026
be76923
Remove ya-passport-auth from requirements
trudenboy Apr 23, 2026
068417e
Remove yandex-music dependency from requirements
trudenboy Apr 23, 2026
43538d5
fix(requirements): restore requirements_all.txt via gen_requirements_…
trudenboy Apr 23, 2026
6048467
feat(yandex_music): sync provider from ma-provider-yandex-music v3.4.1
github-actions[bot] Apr 23, 2026
785fc5e
chore: remove tests/providers/kion_music/test_integration.py
trudenboy Apr 23, 2026
093128a
chore: regenerate requirements_all.txt (yandex-music 3.0.0)
trudenboy Apr 23, 2026
6d934be
Revert "chore: regenerate requirements_all.txt (yandex-music 3.0.0)"
trudenboy Apr 23, 2026
95edc90
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 28, 2026
d1f502a
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 28, 2026
e9ce07a
fix(scripts): sort provider dirs in gen_requirements_all for determin…
trudenboy Apr 28, 2026
0a2c6a2
Revert "fix(scripts): sort provider dirs in gen_requirements_all for …
trudenboy Apr 28, 2026
8200f5c
chore: pin yandex-music==3.0.0 in requirements_all.txt to match CI re…
trudenboy Apr 28, 2026
be10725
chore: flip yandex-music pin back to 2.2.0 (CI inode order shifted ag…
trudenboy Apr 28, 2026
f4ee6cc
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 28, 2026
7f9bdc8
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 28, 2026
cc5e670
Merge branch 'dev' into upstream/yandex_music
trudenboy Apr 28, 2026
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
117 changes: 108 additions & 9 deletions music_assistant/providers/yandex_music/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,30 @@

from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType
from music_assistant_models.enums import ConfigEntryType, ProviderFeature
from music_assistant_models.errors import InvalidDataError

Comment thread
trudenboy marked this conversation as resolved.
from .constants import (
CONF_ACTION_AUTH_QR,
CONF_ACTION_CLEAR_AUTH,
CONF_BASE_URL,
CONF_CODECS,
CONF_LIKED_TRACKS_MAX_TRACKS,
CONF_MY_WAVE_MAX_TRACKS,
CONF_QUALITY,
CONF_REMEMBER_SESSION,
CONF_TOKEN,
CONF_TRANSPORT,
CONF_X_TOKEN,
DEFAULT_BASE_URL,
QUALITY_BALANCED,
QUALITY_EFFICIENT,
QUALITY_HIGH,
QUALITY_SUPERB,
TRANSPORT_ENCRAW,
TRANSPORT_RAW,
)
from .provider import YandexMusicProvider
from .yandex_auth import perform_qr_auth

if TYPE_CHECKING:
from music_assistant_models.config_entries import ProviderConfig
Expand Down Expand Up @@ -55,7 +64,7 @@ async def setup(


async def get_config_entries(
mass: MusicAssistant, # noqa: ARG001
mass: MusicAssistant,
instance_id: str | None = None, # noqa: ARG001
action: str | None = None,
values: dict[str, ConfigValueType] | None = None,
Expand All @@ -64,33 +73,95 @@ async def get_config_entries(
if values is None:
values = {}

# Handle QR auth action
if action == CONF_ACTION_AUTH_QR:
session_id = values.get("session_id")
if not session_id:
raise InvalidDataError("Missing session_id for QR authentication")
x_token, music_token = await perform_qr_auth(mass, str(session_id))
Comment thread
trudenboy marked this conversation as resolved.
Comment thread
trudenboy marked this conversation as resolved.
values[CONF_TOKEN] = music_token
if values.get(CONF_REMEMBER_SESSION, True):
values[CONF_X_TOKEN] = x_token
else:
values[CONF_X_TOKEN] = None

Comment thread
trudenboy marked this conversation as resolved.
# Handle clear auth action
if action == CONF_ACTION_CLEAR_AUTH:
values[CONF_TOKEN] = None
values[CONF_X_TOKEN] = None

# Check if user is authenticated
is_authenticated = bool(values.get(CONF_TOKEN))

# Dynamic label text
if not is_authenticated:
label_text = (
"Scan a QR code with the Yandex app on your phone to authenticate.\n\n"
"Alternatively, you can enter a music token manually in the advanced settings."
)
elif action == CONF_ACTION_AUTH_QR:
label_text = "Authenticated to Yandex Music. Don't forget to save to complete setup."
else:
label_text = "Authenticated to Yandex Music."

return (
# Authentication
# Status label
ConfigEntry(
key=CONF_TOKEN,
type=ConfigEntryType.SECURE_STRING,
label="Yandex Music Token",
description="Enter your Yandex Music OAuth token. "
"See the documentation for how to obtain it.",
required=True,
key="label_text",
type=ConfigEntryType.LABEL,
label=label_text,
),
# QR authentication (primary)
ConfigEntry(
key=CONF_ACTION_AUTH_QR,
type=ConfigEntryType.ACTION,
label="Login with QR code",
description="Opens a QR code page — scan it with the Yandex app on your phone.",
action=CONF_ACTION_AUTH_QR,
action_label="Login with QR code",
hidden=is_authenticated,
value=cast("str", values.get(CONF_TOKEN)) if values else None,
),
# Remember session toggle
ConfigEntry(
key=CONF_REMEMBER_SESSION,
type=ConfigEntryType.BOOLEAN,
label="Remember session (auto-refresh token)",
description="When enabled, stores a long-lived session token to automatically "
"refresh your music token when it expires. When disabled, you must "
"re-authenticate manually when the token expires.",
default_value=True,
hidden=is_authenticated,
),
# Clear auth
ConfigEntry(
key=CONF_ACTION_CLEAR_AUTH,
type=ConfigEntryType.ACTION,
label="Reset authentication",
description="Clear the current authentication details.",
action=CONF_ACTION_CLEAR_AUTH,
action_label="Reset authentication",
hidden=not is_authenticated,
),
# Token storage (populated by QR action or manual entry)
ConfigEntry(
key=CONF_TOKEN,
type=ConfigEntryType.SECURE_STRING,
label="Yandex Music Token",
description="Music token — populated automatically by QR login, "
"or enter manually. See the documentation for how to obtain it.",
required=True,
hidden=is_authenticated,
value=cast("str", values.get(CONF_TOKEN)) if values else None,
),
# x_token (internal storage, always hidden)
ConfigEntry(
key=CONF_X_TOKEN,
type=ConfigEntryType.SECURE_STRING,
label="Session token",
hidden=True,
required=False,
value=cast("str", values.get(CONF_X_TOKEN)) if values else None,
),
# Quality
ConfigEntry(
key=CONF_QUALITY,
Expand All @@ -105,6 +176,34 @@ async def get_config_entries(
],
default_value=QUALITY_BALANCED,
),
# Stream transport (advanced)
ConfigEntry(
key=CONF_TRANSPORT,
type=ConfigEntryType.STRING,
label="Stream transport",
description="Raw streams directly (recommended). "
"Encrypted (encraw) adds AES decryption layer. "
"Both use windowed downloads to prevent CDN drops.",
options=[
ConfigValueOption("Raw (recommended)", TRANSPORT_RAW),
ConfigValueOption("Encrypted (encraw)", TRANSPORT_ENCRAW),
],
default_value=TRANSPORT_RAW,
required=False,
advanced=True,
),
# Codecs override (advanced)
ConfigEntry(
key=CONF_CODECS,
type=ConfigEntryType.STRING,
label="Codecs override",
description="Comma-separated codec priority for get-file-info API. "
"Leave empty to use the default for your quality setting. "
"Example: flac-mp4,flac,aac-mp4,aac,he-aac,mp3,he-aac-mp4",
default_value="",
required=False,
advanced=True,
),
# My Wave maximum tracks (advanced)
ConfigEntry(
key=CONF_MY_WAVE_MAX_TRACKS,
Expand Down
52 changes: 31 additions & 21 deletions music_assistant/providers/yandex_music/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,18 +678,31 @@ async def get_track_download_info(
LOGGER.error("Error fetching download info for track %s: %s", track_id, err)
return []

async def get_track_file_info_lossless(self, track_id: str) -> dict[str, Any] | None:
"""Request lossless stream via get-file-info (quality=lossless).
async def get_track_file_info(
self,
track_id: str,
quality: str = "lossless",
codecs: str = GET_FILE_INFO_CODECS,
transport: str = "raw",
) -> dict[str, Any] | None:
"""Request stream via get-file-info for any quality tier.

The /get-file-info endpoint supports all quality tiers (lossless, nq, lq)
and returns the best available codec based on the codecs parameter order.

Comment thread
trudenboy marked this conversation as resolved.
The /tracks/{id}/download-info endpoint often returns only MP3; get-file-info
with quality=lossless and codecs=flac,... returns FLAC when available.
With transport="raw", returns a direct unencrypted URL.
With transport="encraw", returns an AES-CTR encrypted URL with decryption key.

Uses manual sign calculation matching yandex-music-downloader-realflac.
Uses _call_with_retry for automatic reconnection on transient failures.

:param track_id: Track ID.
:return: Parsed downloadInfo dict (url, codec, urls, ...) or None on error.
:param quality: Quality tier ("lossless", "nq", "lq").
:param codecs: Comma-separated codec preference list.
:param transport: Transport mode ("raw" or "encraw").
:return: Parsed downloadInfo dict (url, codec, key?, ...) or None on error.
"""
# Normalize codecs: strip whitespace from each token to prevent HMAC mismatches
codecs = ",".join(c.strip() for c in codecs.split(",") if c.strip())

def _build_signed_params(client: ClientAsync) -> tuple[str, dict[str, Any]]:
"""Build URL and signed params using current client and timestamp.
Expand All @@ -701,24 +714,20 @@ def _build_signed_params(client: ClientAsync) -> tuple[str, dict[str, Any]]:
params = {
"ts": timestamp,
"trackId": track_id,
"quality": "lossless",
"codecs": GET_FILE_INFO_CODECS,
"transports": "encraw",
"quality": quality,
"codecs": codecs,
"transports": transport,
}
# Build sign string explicitly matching Yandex API specification:
# concatenate ts + trackId + quality + codecs (commas stripped) + transports.
# Comma stripping matches yandex-music-downloader-realflac reference implementation
# (see get_file_info signing in that project).
codecs_for_sign = GET_FILE_INFO_CODECS.replace(",", "")
param_string = f"{timestamp}{track_id}lossless{codecs_for_sign}encraw"
# Build sign string: ts + trackId + quality + codecs (commas stripped) + transports.
codecs_for_sign = codecs.replace(",", "")
param_string = f"{timestamp}{track_id}{quality}{codecs_for_sign}{transport}"
hmac_sign = hmac.new(
DEFAULT_SIGN_KEY.encode(),
param_string.encode(),
hashlib.sha256,
)
# SHA-256 (32 bytes) -> base64 = 44 chars with "=" padding.
# Yandex API expects exactly 43 chars (one "=" removed).
# Matches yandex-music-downloader-realflac reference implementation.
params["sign"] = base64.b64encode(hmac_sign.digest()).decode()[:-1]
url = f"{client.base_url}/get-file-info"
return url, params
Expand Down Expand Up @@ -752,30 +761,31 @@ async def _do_request(c: ClientAsync) -> dict[str, Any] | None:
parsed = _parse_file_info_result(result)
if parsed:
LOGGER.debug(
"get-file-info lossless for track %s: Success, codec=%s",
"get-file-info for track %s: Success, codec=%s, transport=%s",
track_id,
parsed.get("codec"),
transport,
)
return parsed
except (BadRequestError, NetworkError) as err:
LOGGER.debug(
"get-file-info lossless for track %s: %s %s",
"get-file-info for track %s: %s %s",
track_id,
type(err).__name__,
Comment thread
trudenboy marked this conversation as resolved.
Outdated
getattr(err, "message", str(err)) or repr(err),
)
except UnauthorizedError as err:
LOGGER.debug(
"get-file-info lossless for track %s: UnauthorizedError %s",
"get-file-info for track %s: UnauthorizedError %s",
track_id,
getattr(err, "message", str(err)) or repr(err),
)
except Exception as err:
LOGGER.warning(
"get-file-info lossless for track %s: Unexpected error: %s",
"get-file-info for track %s: Unexpected %s: %s",
track_id,
type(err).__name__,
err,
exc_info=True,
)

Comment thread
trudenboy marked this conversation as resolved.
return None
Expand Down
34 changes: 34 additions & 0 deletions music_assistant/providers/yandex_music/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@

# Actions
CONF_ACTION_AUTH = "auth"
CONF_ACTION_AUTH_QR = "auth_qr"
CONF_ACTION_CLEAR_AUTH = "clear_auth"

# QR authentication config keys
CONF_X_TOKEN = "x_token"
CONF_REMEMBER_SESSION = "remember_session"

# Labels
LABEL_TOKEN = "token_label"
LABEL_AUTH_INSTRUCTIONS = "auth_instructions_label"
Expand All @@ -28,6 +33,35 @@
QUALITY_HIGH = "high" # High quality, lossy (~320kbps MP3)
QUALITY_SUPERB = "superb" # Highest quality, lossless (FLAC)

# Transport modes for get-file-info API
CONF_TRANSPORT = "transport"
TRANSPORT_RAW = "raw" # Direct unencrypted stream (default)
TRANSPORT_ENCRAW = "encraw" # AES-CTR encrypted stream

# Custom codecs override (empty = use quality-based default)
CONF_CODECS = "codecs"

# Quality → get-file-info parameter mapping
# Codecs order determines API priority (first codec = preferred by server)
QUALITY_FILE_INFO_PARAMS: Final[dict[str, dict[str, str]]] = {
QUALITY_SUPERB: {
"quality": "lossless",
"codecs": "flac-mp4,flac,aac-mp4,aac,he-aac,mp3,he-aac-mp4",
},
QUALITY_HIGH: {
"quality": "lossless",
Comment thread
trudenboy marked this conversation as resolved.
"codecs": "mp3",
},
QUALITY_BALANCED: {
"quality": "nq",
"codecs": "aac-mp4,aac,mp3,he-aac,he-aac-mp4",
},
QUALITY_EFFICIENT: {
"quality": "lq",
"codecs": "he-aac-mp4,he-aac,aac,mp3",
},
}

# Configuration keys for My Wave behavior (kept)
CONF_MY_WAVE_MAX_TRACKS: Final[str] = "my_wave_max_tracks"

Expand Down
Loading
Loading