All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Reduce Yandex smart-captcha risk for users routing through datacenter or VPN IPs. Tag-validation in browse and recommendations used to fire 5–8 parallel
get_tag_playlistscalls per category viaasyncio.gather. Empirical synthetic probing against the live Yandex backend (4 probes covering sustained RPS, same- and cross-endpoint bursts, and recovery time) found this is harmless on a residential RU IP — Yandex tolerated ≥10 RPS sustained and ≥10 concurrent requests per endpoint without a single 429. On a datacenter / hosted-VPN IP, however, the same burst pattern matched Yandex's scraper-signature heuristic and tripped captcha within ~460 ms. The validation pass was paranoid filtering of evergreen hardcoded tag categories and Yandex's own landing-discovered tags (already pre-filtered server-side); dropping it eliminates the burst entirely. If a tag turns out empty at click time, browse already renders an empty folder.
- Defense-in-depth: per-endpoint concurrency lock in the API client. Every call through
_call_with_retryis now also serialised by a lock keyed on the callingYandexMusicClientmethod, so a futureasyncio.gatherover the same method cannot re-introduce the burst pattern. Per-kind throttler continues to cap sustained RPS; the lock caps in-flight requests to ≤1 per endpoint family. Distinct methods still run concurrently subject only to the throttler. Cost is zero in steady-state — only matters when something fans out. - New advanced provider setting "Restrictive rate limits" (off by default). Enables a token-wide global concurrency cap (≤5 in-flight requests across all kinds and endpoints) for users running Music Assistant on a VPS, NAS-behind-VPN, or any datacenter IP. Synthetic probing against the live Yandex backend measured the edge-layer concurrency limit at 6 simultaneous requests on a Swedish hosting IP (vs ≥10 on residential RU); the cap is sized one below that ceiling. Residential users do not need this — Yandex tolerates much higher concurrency from regular ISPs.
- Tighten the captcha cooldown ladder from
(60s, 300s, 600s)to(15s, 60s, 120s). Synthetic probing showed Yandex releases a tripped token in ~15 seconds; the previous ladder kept the provider blocked for ~4× longer than Yandex's actual edge memory required. - Raise the per-kind throttler budgets:
THROTTLE_DEFAULT_RPS3 → 5 andTHROTTLE_METADATA_RPS2 → 3. Probing confirmed Yandex tolerates sustained sequential ≥10 RPS on both residential and datacenter IPs; the previous caps were over-conservative. - Reshape the seasonal-mix recommendation fallback: try the current season's tag, fall back to
autumnonly if the first call returns zero playlists. Previously a separate runtime validation call was made first; the new path makes at most one extra request when actually needed. - Forensic logging of every 429-classified response (
markers_matched+ first 2000 chars of body) and per-requestcaller=/kind=diagnostic remain in the code at DEBUG level so a single captcha trip can be reconstructed by flipping the provider log level — no impact on steady-state logs.
- Enable stale-while-revalidate (
allow_expired_cache=True) on every@use_cachemethod in the provider. Browse, library, search, recommendations, artist/album/track detail, and rotor wave-mode lookups now return the cached result immediately even when expired, and refresh in the background — so navigation stays snappy on slow networks or during a rate-limit cooldown. Restores the upstream Music Assistant convention that was lost during the sync of earlier releases.
- Detect Russian-language bios correctly when the text has leading or trailing whitespace — padding no longer dilutes the Cyrillic-share check below threshold.
- Rank unknown-bitrate audio variants unambiguously when "efficient" quality is selected — lossless streams (reported as bitrate 0) and unknown-bitrate streams now both rank last so they never out-rank a real low-bitrate AAC.
- Drop the unused
limitargument from the internal Yandex search API call. Callers slice each result bucket to their requested size after classification; the lower-level call never honoured a per-type cap. - Tighten the return-type annotation of the internal "URL refresh on 4xx" helper to reflect actual behaviour: it always returns
bytes(empty for raw streams) or raisesMediaNotFoundError. - Use a defensive
getattrfor the Yandex clientbase_urlattribute in the landing-blocks endpoint, mirroring the rotor-session helper. A library rename will degrade gracefully instead of raisingAttributeError. - Introduce
CONF_SESSION_IDconstant for the session-id config key used by both auth flows, replacing two bare string literals. - Apply
Finalannotations uniformly to module-level constants for stricter type-checking.
- Unused legacy auth-flow constants (
CONF_ACTION_AUTH,LABEL_TOKEN,LABEL_AUTH_INSTRUCTIONS) and the unusedIMAGE_SIZE_SMALLimage-size constant.
- Improve test coverage for stream-details resolution, podcast / audiobook / episode parsers, and the recommendations orchestrator. Closes coverage gaps flagged by the codebase review:
get_stream_detailsnow has happy-path, download-info-fallback, and both-fail tests; thedescription_language is Noneinvariant is regression-guarded on podcast / audiobook / episode parsers; and the My Wave recommendations folder is end-to-end tested against the realparse_trackpath so a parser/orchestrator integration regression cannot slip through.
- Reduce search-result staleness from 14 days to 1 day so newly released albums appear within a day of being added to Yandex's catalogue.
- Stop discarding already-fetched playlist tracks when a single batch mid-load returns empty. The playlist now loads partially and a warning is logged; the existing all-batches-empty guard still raises terminal failures.
- Surface 4xx responses from search, liked-tracks, and liked-albums correctly. Previously these were wrapped as transient errors and Music Assistant retried in a loop reproducing the same failure; now they resolve to empty results.
- Distinguish transient network failures from invalid credentials in token validation. Good tokens are no longer dropped on a network hiccup; only Passport's terminal rejection clears them.
- Sort liked tracks safely when the upstream library returns timezone-naive timestamps for some entries and timezone-aware for others — the whole liked-tracks collection could previously fail to load with a
TypeError. - Apply rate-limit throttling consistently on the reconnect-retry path. Skipping the throttler acquire on the retry attempt doubled the effective request rate during connection flap, increasing Yandex smart-captcha trip risk.
- Skip the unnecessary reconnect cycle on 4xx errors.
BadRequestErroris a terminal client error from Yandex; it no longer triggers a connection reset and retry. - Clear all per-session caches (My Wave state, wave background colors, liked-albums cache) on provider reload so stale
asyncio.Lockobjects bound to the previous event loop cannot survive a settings change.
- Stop including upstream library exception detail in user-facing authentication errors. Yandex Passport failures (login, token refresh, credential refresh) now surface only the error class name, so token fragments or response bodies that the library may embed in its exception messages can no longer reach Music Assistant logs or the frontend.
- Stop embedding the signed CDN URL in stream-fetch error messages. When a Yandex audio request fails with an HTTP error, the resulting
MediaNotFoundErrornow reports only the HTTP status — the URL's expiring signature no longer leaks into logs.
- Limit
description_languagetagging to artist biographies only. The previous release also tagged playlist / podcast / podcast-episode / audiobook descriptions, but Music Assistant currently has no metadata-provider competition for those text fields, so the language hint never affected what users see. The artist case — where multiple sources (Yandex, TheAudioDB, fanart, lastfm) compete and the user's preferred-language setting actually drives the merge — is preserved.
- Tag Russian-language descriptions (artist bios, playlists, podcasts, episodes, audiobooks) with the new upstream
description_languagefield so Music Assistant keeps the Yandex copy for Russian-speaking users instead of falling back to a metadata-provider bio in another language. Detection is based on Cyrillic script dominance; non-Russian or ambiguous text is left unset.
- Reduce smart-captcha rate-limit lockouts during initial library sync: the first captcha trip now uses a 60-second cooldown that escalates only on repeated trips inside the same hour (60s → 300s → 600s). Previously a single captcha event blocked the provider for a full 10 minutes.
- Isolate artist and album metadata refresh from the rest of the API so a hot metadata burst no longer blocks search, playlists, or playback.
- Add a small jitter to API calls during the first minute after connect to smooth out the parallel metadata-refresh burst Music Assistant triggers immediately after a fresh install.
- Lower the default per-second request budget so initial library sync is less likely to trip Yandex's smart-captcha edge in the first place.
- Refresh parser test snapshots for new upstream
music_assistant_modelsfields (description_language,proxy_idon images) so CI passes without behavioral changes.
- fix(parsers): widen
Audiobook.authorstype annotation toUniqueList[str | Artist]to match upstreammusic_assistant_modelsand unblock mypy under upstream-synced typing rules. - fix(tests/snapshots): refresh
test_parsers.ambrfor the upstreammusic_assistant_modelsevolution —Artist.artist_type(new field, defaultsinger) and the newMediaItemMetadatafields (review,mood,style,lyrics,lrc_lyrics,performers,preview,popularity,release_date). - chore: sync workflow wrappers from ma-provider-tools (CLAUDE.md, ruff/mypy/codespell from upstream music-assistant/server, config-sync CI guard).
- feat(audiobook): sync progress to Yandex, surface in search and browse (#115) (
0e9cda6) - chore: update changelog for v3.2.1 [skip ci] (
2bbc168)
- fix(search): apply limit per bucket after classify_album (#116) (
272461a) - chore: update changelog for v3.3.0 [skip ci] (
ec79132)
- feat(rotor): migrate My Wave to session API + wave modes, presets, prefetch (#118) (
529b686) - chore(changelog): remove duplicate 3.3.0 and 3.3.1 entries (#117) (
7a89c08) - chore: update changelog for v3.3.1 [skip ci] (
8d73362)
- fix(rotor): surface UnauthorizedError on /rotor/session/* as LoginFailed (#120) (
c08adfa) - fix(presets): drop whitespace-only dropdown values + strip surrounding spaces (#119) (
d375a0d) - chore: update changelog for v3.4.0 [skip ci] (
41f2932)
- chore: sync workflow wrappers from ma-provider-tools (#130) (
a0938ab) - chore: sync workflow wrappers from ma-provider-tools (#127) (
5917238) - chore: sync workflow wrappers from ma-provider-tools (#125) (
bd264b8) - chore: sync workflow wrappers from ma-provider-tools (#123) (
9ec09c5) - chore: update changelog for v3.4.1 [skip ci] (
9357d95)
- chore: bump VERSION to 3.5.1 (
dbbc460) - chore: update changelog for v3.5.0 [skip ci] (
e447034) - style: auto-fix ruff (
2ea0a1b)
- fix(rate-limit): cooldown re-check on retry + cache awareness during cooldown (
759791c) - style: auto-fix ruff (
a198cb9)
- style: auto-fix ruff (
c8f9abd)
- chore: update changelog for v3.5.7 [skip ci] (
69f4cdf)
- fix(resilience): distinguish transient from terminal errors across the stack (#158) (
1901fbf) - style: auto-fix ruff (
fd1ad0b)
- test: close coverage gaps in stream-details, parsers, and recommendations (#160) (
f4c73f3) - chore: update changelog for v3.5.11 [skip ci] (
bd7be94)
- chore(modules): hygiene pass — Final annotations, dead constants, idioms (#162) (
a8905ed) - chore: update changelog for v3.5.12 [skip ci] (
0615505)
- feat(rate-limit): captcha-aware retry, per-kind throttling, file-info cache (#140) (
d821cb3) - chore: sync workflow wrappers from ma-provider-tools (#139) (
bf8b6be) - chore: sync workflow wrappers from ma-provider-tools (#138) (
6af7612) - chore: sync workflow wrappers from ma-provider-tools (#137) (
b1cacca) - chore: sync workflow wrappers from ma-provider-tools (#136) (
67d7bc6) - chore: sync workflow wrappers from ma-provider-tools (#135) (
c3e17a0) - chore: sync workflow wrappers from ma-provider-tools (#134) (
fdc4b91) - chore: sync workflow wrappers from ma-provider-tools (#132) (
bf27ce9) - chore: update changelog for v3.4.2 [skip ci] (
974c6c7)
- fix(audiobook): wire seek through can_seek and bound chapter failures (
40bdc9b) - chore: update changelog for v3.2.0 [skip ci] (
f499ea4)
- feat(audiobook): stream audiobooks via chapter concatenation (
d9f483c) - chore: update changelog for v3.1.2 [skip ci] (
d10fffb)
- fix(auth): clear stale refresh_token on QR re-auth (
48d50c4) - chore: update changelog for v3.1.1 [skip ci] (
4809461)
- chore: bump version to 3.1.1 (
917f43e) - fix(auth): wrap transient Passport errors in _reauth_via_refresh_token (
5ad9fff) - chore: expand changelog for v3.1.0 [skip ci] (
55da091) - chore: update changelog for v3.1.0 [skip ci] (
0f91ad1)
- feat(library): podcasts + audiobook classifier (Phase 1) — liked albums routed to music/podcast/audiobook views via
meta_type/type; podcasts fully wired (list, detail, episodes, like/unlike, search, stream); audiobook chapters fromalbum.volumes; audiobook streaming raisesNotImplementedErrorpending Phase 2 (#112) (e0c27c9) - feat(library): short-lived
_get_liked_albums_cached(30s, asyncio.Lock) dedupes the three library syncs into one API call per cycle (#112) (e0c27c9) - fix(classify): audiobooks in production are
meta_type=podcast+type=audiobook— "audiobook" now wins over "podcast" on any field (#112) (e0c27c9) - fix(parsers): copy parent podcast images into episode instead of sharing the mutable
UniqueListreference (#112) (e0c27c9) - fix(podcast):
get_podcast_episoderaisesInvalidDataErrorwhen parent album is missing — noitem_id="unknown"fallback (#112) (e0c27c9) - fix(features): restore
ProviderFeature.SIMILAR_ARTISTS(regression from Phase 1 rebase) (#112) (e0c27c9) - fix(quality): normalize legacy 'lossless' to QUALITY_SUPERB in get_quality (
935a518) - fix(auth): don't clear creds on transient Passport failures (
31abe95)
- Update VERSION (
dc9c2bb) - fix: address PR review comments (
41d1709) - fix: address PR review comments (
bad8a70) - test: remove integration test module (
7d82a5a) - fix(manifest): drop [async] extra from yandex-music requirement (
853fa42) - fix(device-auth): don't log user_code at INFO level (
24b211c) - chore: clean up CHANGELOG — order, dedup, noise [skip ci] (
50bc67e) - Bump version from 2.9.1 to 3.0.0 (
278b2b8) - chore: update changelog for v3.0.0 [skip ci] (
fe1de36)
- feat: migrate to yandex-music v3 + Pinned/History browse + Device Flow auth (#102) (
5d4a54f) - refactor: merge device_auth + yandex_auth into single auth module (#109) (
5bcaa16) - fix(streaming): accept camelCase downloadInfo from yandex-music v3 (#106) (
f29fe2e) - fix(streaming): restrict can_seek to byte-seekable codecs (
07001a5) - fix(device-auth): show user code in MA-served intermediate page (#104) (
bacaec2) - fix(device-auth): readable popup + auto-close after authorization (#107) (
b54f6ea)
- feat(streaming): native seek support for raw transport (#88) (
afc589a) - feat: add get_rotor_station_tracks and get_quality wrapper methods (
b84630b) - fix(streaming): only set can_seek when byte offset is computable (
b24cadb) - fix: re-raise CancelledError in get_track_file_info, fix docstring (
d04d453) - fix(ci): add runtime deps to pyproject.toml for test discovery (
cf6424f) - fix(dev): use unsafe-best-match index strategy for dev deps (
6d5bccd) - fix: make CONF_TOKEN required to prevent saving without auth (
acc0c5e) - fix: raise InvalidDataError for missing session_id in QR auth (
a331238) - fix: pin ya-passport-auth==1.0.0 in manifest.json (
6af4819) - chore: bump ya-passport-auth to 1.2.3 (
2865bd8) - chore: update Python version in mypy configuration (
044407a)
- feat: replace manual QR auth with ya-passport-auth library (#85) (
8529ce2) - chore: switch ya-passport-auth to stable PyPI release (#87) (
5e3824d) - fix: handle ValueError in hex key decoding and TimeoutError in auth (
900c8a1) - fix: correct MP4 sample_rate offset and re-raise CancelledError in probe (
fcde68c) - fix: update stream detail assertions to match CUSTOM stream type (
dd245e4) - fix: mock get_track_file_info in integration tests to prevent coroutine error (
4e49fd0)
- refactor: unify streaming under get-file-info with raw transport (#71) (
a89972b) - fix: read bitrate from API and use lossless quality for MP3 320 (
b5f6a24) - fix: parse real audio params from API and container headers (
6f45425) - fix: auto-parse codec strings so content_type reflects audio codec, not container (
27b830a) - fix: make CONF_TOKEN required to prevent saving without credentials (
95118d3) - fix: use or-operator instead of ternary for FURB110 lint rule (
5ba024e) - fix: address Copilot review feedback on QR auth PR (
09dda3b)
- fix: try multiple CSRF token extraction patterns for Yandex Passport (
f6f7817)
- fix: add mobile User-Agent to QR auth requests (
57ac5b8)
- feat: QR-code authentication as primary login method (#68) (
60140d6) - fix: update playlist snapshots for MA models 1.1.110 (is_dynamic field) (
fc2c0d1) - fix: use received HTTP bytes (not decrypted) for EOF detection in windowed stream (
a038107) - fix: detect EOF at exact _RANGE_WINDOW boundary via Content-Range header (
fd3e26f) - refactor: extract _handle_stream_error to fix PLR0915 in get_audio_stream (
9bbdc8e) - chore: improve manifest description and add credits field (
c396505)
- refactor: extract _decrypt_response_stream to fix PLR0915 (
f8cc382) - fix: address PR review comments — URL refresh improvements (
54ad23e) - test: update playlist snapshots for supported_mediatypes field (
08f8f63) - chore: skip docs-site/package-lock.json in codespell (
bbcebf5) - fix: use _track_id_from_item_id helper in _refresh_encrypted_url (
010db22) - fix: raise max_retries from 5 to 6 for encrypted stream (
6a7469a) - docs: document Yandex CDN ~6-7 MB per-connection limit and 6-retry coverage (
3893225) - fix: fix infinite loop bug on 4xx URL refresh in windowed stream loop (
5eb8a33) - feat: replace open-ended Range requests with 4 MB windowed streaming (
f86e4fd) - docs: remove CDN limitation section, streaming now uses 4 MB windows (
fc9a814) - chore: sync workflow wrappers from ma-provider-tools (#64) (
d8e11b9)
- fix: prevent lossless playback interruptions without buffering (
2b2d630) - fix: harden encrypted stream against more failure modes (
d4cc27f) - fix: raise max_retries from 3 to 5 for encrypted stream (
6b3b2e6)
- fix: address PR review comments on throttling (
1887dd7)
- chore: sync workflow wrappers from ma-provider-tools (#61) (
77dd47d) - docs: add Starlight index page (
db17ee4) - docs: add configuration page (
147a341) - docs: add my-wave feature page (
3f3e049) - docs: add lyrics feature page (
0fe8704) - docs: add picks-and-mixes feature page (
853c796) - docs: add browse feature page (
b06c58b) - docs: add audio-quality feature page (
915221a) - chore: add package-lock.json for npm cache (
5f932a3) - docs: add Starlight title frontmatter to contributing.md (
8bfc084) - docs: add development.md (dev environment guide) (
4c502e4) - docs: add provider icon emblem to index page (
4faec66) - docs: обновление пользовательской документации (#62) (
19d4c1c) - docs: добавить способ получения токена из десктопного клиента (#63) (
9af7301) - fix(docs): исправить форматирование таблицы на странице radio (
6226490) - docs: добавить ссылку на библиотеку и дисклеймер на главной странице (
eff8325) - docs: упростить дисклеймер на главной странице (
3aba38f) - docs: добавить английские переводы всех страниц документации (
7d1e100) - docs: сделать Music Assistant ссылкой на официальный сайт (
e6bb739) - Reorder and update Yandex Music provider information (
f336914) - Change warning to caution in disclaimer section (
674f8d3) - Add subscription note for Yandex Plus (
a756ffe) - Fix markdown syntax for caution block (
aab1d30) - Revise disclaimer for unofficial Yandex Music provider (
65c0259) - feat: add request throttling and rate-limit detection (
a6883f2) - feat: throttle _call_no_retry (rotor feedback) via shared Throttler (
2201dd4) - chore: remove redundant ruff.toml and ignore docs-site/node_modules (
3597f16) - chore: add docs-site/node_modules to gitignore, trim trailing newlines (
501f72d)
- feat: add WEB_BASE_URL constant for web UI links (
413d616) - refactor: use WEB_BASE_URL constant in parsers instead of hardcoded URLs (
7820b16)
- fix: use DEFAULT_BASE_URL constant in description string (
7cca832)
No changes.
No changes.
- chore: sync workflow wrappers from ma-provider-tools (#59) (
25dbf7b)
- Refactor/yandex config categories (#50) (
500de64) - chore: sync workflow wrappers from ma-provider-tools (#51) (
6069c9e) - chore: sync workflow wrappers from ma-provider-tools (#52) (
4c8d318) - chore: sync workflow wrappers from ma-provider-tools (#53) (
071d2f7) - chore: generate historical changelog [skip ci] (
4e39430) - docs: add pre-separation upstream PR history to CHANGELOG (
df2bb6a) - chore: sync workflow wrappers from ma-provider-tools (#54) (
c545e94) - chore: sync workflow wrappers from ma-provider-tools (#55) (
694565f) - docs: remove FLAC streaming modes and buffer size documentation (#56) (
df58fed) - chore: sync workflow wrappers from ma-provider-tools (#57) (
f926e41) - chore: sync workflow wrappers from ma-provider-tools (#58) (
fe49532)
- Refactor/yandex config categories (#50) (
500de64)
- fix: add mass fixture to conftest.py and remove stale type: ignore (
62ebd9d) - fix: fix ruff import sort in tests/common.py (
eae9d6a) - fix: bump music-assistant-models to 1.1.99 and add tests/common.py (
6a44330) - docs: unify documentation structure (#16) (
bb8827b)
- fix(streaming): fix FLAC seek, remove direct/preload modes (
388a3cd) - feat: initial provider setup (
d5cf489)
- fix: validate tags via API before showing in browse and recommendations
- fix: fix LRC regex, HMAC sign construction, and temp file cleanup order
- fix: fix race conditions, dead code, caching bugs, and performance issues
- fix: fix exception handling, docstring, and log noise in provider
- fix: drastically reduce config entries and simplify configuration
- fix: fix critical review issues: rename key_base64, fix double close, add temp file cleanup
- fix: fix streaming issues: FD leak, buffered fallback, size limits, task cleanup, stale files
- fix: fix AAC codec mapping, caching issues, and datetime timezone awareness
- fix: fix Picks & Mixes: use dynamic tag discovery, filter empty/useless tags
- fix: fix browse: "Invalid subpath" error and empty tag folders
- fix: increase Discovery initial tracks from 5 to 20
- test: add comprehensive test coverage for recommendation methods
- test: restore test_recommendations.py with mypy return type annotations
- feat: add My Wave visibility toggle settings
- feat: add My Wave (Моя волна) Browse folder
- fix: fix playlist loading and missing album cover art (music-assistant/server#3099)
- chore: regenerate parser snapshots after upstream models bump
- fix: fix playlist tracks not loading in UI
- fix: fix missing album cover art in library
- fix: suppress library DEBUG logs to avoid huge API dumps in playlist view
- fix: load playlist tracks reliably and avoid timeouts
- fix: prevent caching of empty playlist results on network errors
- chore: remove lyrics feature support
- feat: add Yandex Music provider (music-assistant/server#3002)