feat(yandex_music): improve configuration with My Wave, Liked Tracks, and organized settings#3147
feat(yandex_music): improve configuration with My Wave, Liked Tracks, and organized settings#3147trudenboy wants to merge 51 commits intomusic-assistant:devfrom
Conversation
Add 6 new configuration options for Yandex Music provider: - My Wave maximum tracks (default: 150) - Control total number of tracks fetched - My Wave batch count (default: 3) - Number of API calls for initial load - Track details batch size (default: 50) - Batch size for track detail requests - Discovery initial tracks (default: 5) - Initial display limit for Discover - Browse initial tracks (default: 15) - Initial display limit for Browse - Enable Discover (default: true) - Toggle recommendations on/off Implemented duplicate protection for My Wave tracks using set-based tracking. Recommendations now refresh every 60 seconds instead of 3 hours for fresher discoveries. All new settings have sensible defaults that maintain current behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Advanced setting for API base URL in Yandex Music provider, allowing users to change the API endpoint if service updates their URL. Changes: - Add CONF_BASE_URL and DEFAULT_BASE_URL constants - Add Advanced ConfigEntry for base_url in provider settings - Update YandexMusicClient to accept base_url parameter - Pass base_url from config to ClientAsync initialization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rting This commit adds three major improvements to the Liked Tracks feature: 1. Reverse chronological sorting: - Liked tracks are now sorted by timestamp (most recent first) - Matches mobile app behavior for better UX - Applied automatically in get_liked_tracks() method 2. Browse folder visibility toggle: - Added CONF_ENABLE_LIKED_TRACKS_BROWSE config option - Allows hiding Liked Tracks folder from Browse section - Default: True (backward compatible) 3. Virtual playlist for Liked Tracks: - Added LIKED_TRACKS_PLAYLIST_ID virtual playlist - Appears in library playlists (similar to My Wave) - Supports full MA playlist features (radio, favorites, etc.) - Configurable via CONF_ENABLE_LIKED_TRACKS_PLAYLIST - Respects CONF_LIKED_TRACKS_MAX_TRACKS limit (default: 500) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…es and advanced flags Restructures the 16 Yandex Music provider configuration entries to improve UX: - Groups My Wave settings (6 entries) in "my_wave" category - Groups Liked Tracks settings (3 entries) in "liked_tracks" category - Marks performance tuning settings as advanced (7 entries total) - Maintains authentication/quality settings at top level This reduces visible clutter from 16 to ~8 settings by default, with advanced options hidden behind a toggle. No breaking changes - all config keys, defaults, and functionality remain unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🔒 Dependency Security Report📦 Modified Dependencies
|
…ryption This commit fully implements lossless FLAC playback for Yandex Music, fixing the issue where only MP3 320kbps was available despite user having a premium subscription. ## Changes ### Quality Levels Restructure - Replaced HIGH/LOSSLESS quality options with three-tier system matching reference implementation - EFFICIENT (AAC ~64kbps) - Low quality, efficient bandwidth - BALANCED (AAC ~192kbps) - Medium quality (new default) - SUPERB (FLAC Lossless) - Highest quality - Updated manifest.json and config entries to reflect new quality options ### API Authentication Fix - Implemented manual HMAC-SHA256 sign calculation matching yandex-music-downloader-realflac - Critical fix: Remove last character from base64-encoded sign ([:-1]) - Fixed HTTP 403 "not-allowed" errors from /get-file-info endpoint - Uses DEFAULT_SIGN_KEY from yandex-music library ### FLAC Decryption Implementation - Added _decrypt_track_url() method using AES-256 CTR mode - Uses PyCrypto (pycryptodome) which supports 12-byte nonce for CTR mode - Key is HEX-encoded (bytes.fromhex), not base64 as initially attempted - Downloads encrypted stream, decrypts on-the-fly, saves to temp file - Returns StreamDetails with StreamType.LOCAL_FILE pointing to decrypted temp file ### Streaming Logic Updates - Enhanced get_stream_details() to handle encrypted URLs from encraw transport - Detects needs_decryption flag in API response - Falls back gracefully to MP3 if decryption fails - Supports both encrypted and unencrypted FLAC URLs - Updated _select_best_quality() to intelligently select based on three quality tiers ### Dependencies - Added pycryptodome==3.21.0 to support AES CTR mode with 12-byte nonce - Uses aiohttp for direct HTTP download of encrypted streams ### Testing - All existing tests pass (7/7 in test_streaming.py) - Type checking passes (mypy success) - Code quality checks pass (ruff linter/formatter) ## Technical Details The Yandex Music API returns encrypted URLs when using transports=encraw. The decryption process matches the working reference implementation: 1. Calculate HMAC-SHA256 sign with all param values joined 2. Base64 encode and remove last character (critical!) 3. Request /get-file-info with quality=lossless, codecs=flac-mp4,flac,... 4. Download encrypted stream from returned URL 5. Decrypt using AES-256 CTR with 12-byte null nonce and HEX-decoded key 6. Save decrypted FLAC to temporary file 7. Return stream details pointing to temp file ## Tested - Server starts successfully with Yandex Music provider loaded - FLAC codec detected from API (codec=flac-mp4) - Encryption detected and decryption executes (55MB encrypted → decrypted) - StreamType.LOCAL_FILE allows playback from temp file - Graceful fallback to MP3 if decryption fails Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace temp file approach with memory-based streaming decryption for better performance and reduced disk I/O. ## Changes ### StreamType.CUSTOM Implementation - Use AsyncGenerator[bytes, None] for streaming decryption - No temporary files on disk - all processing in memory - Store encrypted URL and key in StreamDetails.data ### Streaming Decryption Pipeline - Download encrypted stream in 64KB chunks using aiohttp - Decrypt incrementally with AES-256 CTR mode - Counter auto-increments for each block (streaming-friendly) - Yield decrypted chunks directly to audio pipeline ### Performance Improvements - **First chunk:** 0.39s vs 5+ seconds with temp file approach - **No disk I/O:** Streaming directly from memory - **Lower latency:** Start playback while downloading/decrypting - **Efficient:** 64KB chunks balance memory and throughput ### Implementation Details - Added get_audio_stream() method to YandexMusicStreamingManager - Cipher initialization with 12-byte null nonce (PyCrypto) - ClientTimeout: connect=30s, sock_read=600s for stable streaming - Proper error handling and logging throughout pipeline ### Technical Notes AES CTR mode is ideal for streaming because: - Each block can be encrypted/decrypted independently - Counter increments automatically - no state management needed - Supports arbitrary chunk sizes (not just 16-byte blocks) ## Tested - All 7 unit tests pass - Type checking passes (mypy) - Code quality checks pass (ruff) - Live streaming confirmed: codec=flac, streamtype=custom - First audio chunk in <0.4s Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pywidevine==1.9.0 requires pycryptodome>=3.23.0 Updated from 3.21.0 to 3.23.0 to satisfy both dependencies
Add new "High" quality level between Balanced and Superb: - Efficient (AAC ~64kbps) - Low quality - Balanced (AAC ~192kbps) - Medium quality - High (MP3 ~320kbps) - High quality lossy (NEW) - Superb (FLAC Lossless) - Highest quality Implementation: - Add QUALITY_HIGH constant in constants.py - Add "High (MP3 ~320kbps)" option in config UI - Update _select_best_quality() logic to prefer MP3 >=256kbps - Fallback chain: high bitrate MP3 → any MP3 → highest non-FLAC Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On low-power devices, encrypted FLAC streaming breaks after ~1 minute because AES decryption can't keep up with download speed, causing the server to drop the connection (ClientPayloadError). Add 3 configurable streaming modes to decouple download from decryption: - Direct: on-the-fly decrypt (original behavior, fast devices) - Buffered: async queue with backpressure (recommended, default) - Preload: full download then decrypt via SpooledTemporaryFile (slow devices) Closes #29 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_ensure_connected() now attempts reconnect when _client is None instead of raising immediately. Added _call_with_retry() helper that wraps API calls with one reconnect attempt on connection errors. Refactored all API methods to use it, eliminating ad-hoc retry loops. Fixes permanent provider death after temporary network outage where _reconnect() failure set _client=None with no recovery path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, and playlists Add 4 new recommendation sections beyond My Wave: Made for You (personalized feed playlists), Chart (top tracks), New Releases (albums), and New Playlists (editorial). Each section has its own config toggle under a "Discovery" category and independent cache TTLs (30min for feed, 1h for chart/releases/playlists). Closes #34 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Preload mode now downloads and decrypts to temp file before playback - Returns StreamType.LOCAL_FILE with can_seek=True for proper navigation - Files exceeding size limit (config) fall back to Buffered mode - Rename config 'Preload memory limit' to 'Preload max file size' with updated description - Add temp file cleanup on stream completion and provider unload Fixes: seek not working when using Superb quality + Preload mode Fixes: progress bar starting before audio is ready Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Browse: - picks/ folder with mood, activity, era, genres subfolders - mixes/ folder with seasonal collections (winter, summer, autumn, newyear) - Each tag returns curated playlists from Yandex Music tags API Discovery (home page): - Top Picks: curated playlists (tag: top) - Mood Mix: rotating mood playlists (chill, sad, romantic, party, relax) - Activity Mix: rotating activity playlists (workout, focus, morning, evening, driving) - Seasonal Mix: playlists based on current season Configuration (new picks_mixes category): - Enable Picks in Browse - Enable Mixes in Browse - Enable Top Picks on Home - Enable Mood/Activity/Seasonal Mix on Home Localization: - All folder and tag names in Russian and English Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Add ProviderFeature.LYRICS to supported features - Add get_track_lyrics() method to api_client.py - Extend parse_track() to accept lyrics and lyrics_synced parameters - Fetch lyrics in get_track() and attach to track metadata - Support both synced LRC format and plain text lyrics - Handle geo-restrictions and unavailable lyrics gracefully Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Split get_playlist() to separate virtual playlists (not cached) from real ones - Virtual playlists (My Wave, Liked Tracks) now always use current locale - Real playlists continue to be cached for 30 days - Add debug logging for locale detection Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Fix base path calculation in _browse_picks() and _browse_mixes() - Previously: base was incorrectly trimming to parent path - Now: base correctly appends to current path - Add debug logging for troubleshooting Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ests The lyrics feature added in 4fc11fd introduced a get_track_lyrics call in get_track, but the test fixtures were not updated with the mock, causing ValueError on tuple unpacking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…le cleanup (#54) * Initial plan * Fix LRC regex, HMAC sign construction, and temp file cleanup order Co-authored-by: trudenboy <139659391+trudenboy@users.noreply.github.com> * Fix linting issues in tests Co-authored-by: trudenboy <139659391+trudenboy@users.noreply.github.com> * Fix spelling in test comments Co-authored-by: trudenboy <139659391+trudenboy@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: trudenboy <139659391+trudenboy@users.noreply.github.com>
MarvinSchenkel
left a comment
There was a problem hiding this comment.
The decryption logic needs to be rethought. We can't cache entire tracks. I will have another look at this PR when that has been refactor 👍
…ate import - Use == instead of is for dict comparison (BROWSE_NAMES_RU) - Remove duplicate AsyncGenerator import from TYPE_CHECKING block Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test_temp_file_replacement_order test was failing because MinimalProvider lacked client and mass attributes expected by YandexMusicStreamingManager.__init__. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@MarvinSchenkel Thanks for the review! I've addressed the Regarding "caching entire tracks" — this only happens in Preload mode, which is:
The default Buffered mode streams and decrypts on-the-fly via an async queue without caching entire tracks. The Direct mode does the same but without the queue buffer. Preload is simply an additional option for edge cases where real-time processing isn't feasible. Preloaded temp files are never left behind — there's a 3-level cleanup mechanism:
Update: Also fixed container handling for
Happy to discuss further or adjust anything! 🙏 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Address Copilot review comments on PR music-assistant#3147: - _browse_my_wave: cap fetch loop to BROWSE_INITIAL_TRACKS on initial browse instead of post-loop slicing, preventing tracks from being marked as "seen" but never shown to the user - _get_discovered_tags: add locale parameter to cache key so tag titles are re-fetched when locale changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Copilot review comments on PR music-assistant#3147: - _browse_my_wave: cap fetch loop to BROWSE_INITIAL_TRACKS on initial browse instead of post-loop slicing, preventing tracks from being marked as "seen" but never shown to the user - _get_discovered_tags: add locale parameter to cache key so tag titles are re-fetched when locale changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Yandex API returns codec "flac-mp4" meaning FLAC audio inside an MP4 container. Previously _get_content_type mapped this to ContentType.FLAC, causing ffmpeg to misidentify the container format. Now correctly returns (ContentType.MP4, ContentType.FLAC) as container/codec pair, matching how Apple Music handles MP4+AAC. Also fixes temp file extension for preload mode (.mp4 instead of .flac). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follow-up to b6b11a2 (flac-mp4 container fix). Replaces hardcoded "FLAC" in log messages with the actual codec string so logs correctly reflect flac-mp4 vs flac. Renames TEMP_FILE_PREFIX to "yandex_audio_" since temp files may now be .mp4. Adds clarifying comment about flac-mp4 in _select_best_quality. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Yandex API does not return sample rate or bit depth, so AudioFormat used model defaults (44100/16) even for flac-mp4 streams that are actually 48kHz/24bit. This caused unnecessary downsampling in _select_pcm_format. Add codec-based defaults via _get_audio_params and _build_audio_format helpers to set the correct values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase default buffer from 2MB (32 chunks) to 8MB (128 chunks) to reduce stuttering on slow/unstable connections (~45s of FLAC audio). Add CONF_STREAM_BUFFER_MB config entry (1-32 MB, default 8) so users can tune the buffer size for their network conditions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Major improvements to Yandex Music provider: FLAC lossless playback, Lyrics support, Picks & Mixes curated collections, extended recommendations, and streamlined configuration.
Scope: 11 files changed, ~3300 lines added, ~400 removed. 94 unit tests.
New Features
FLAC Lossless Playback
Lossless audio streaming with AES-256-CTR decryption for premium subscribers via
/get-file-infoAPI with HMAC-SHA256 signed requests. Lossless requests use_call_with_retryfor automatic reconnection on transient failures — HMAC signature is recomputed with fresh timestamp on each retry.Quality tiers:
Streaming modes (for encrypted FLAC):
Preload mode auto-falls back to Buffered when file exceeds configurable size limit.
Lyrics
Native lyrics from Yandex Music API:
\[\d{2}:\d{2}regexPicks & Mixes (Curated Collections)
Browse access to Yandex Music curated playlists with runtime tag validation:
How it works:
landing("mixes"))client.get_tag_playlists()— only tags with actual playlists are shownasyncio.gather+Semaphore(8)) to minimize latency/post/URLs from landing API are filtered out (only/tag/entries kept)TAG_SLUG_CATEGORYmapping (unknown tags default to Mood)TAG_CATEGORY_ORDERfor consistent displayis_playable=Falseto prevent accidentally loading thousands of tracks_validate_tag()Extended Recommendations (Home Page)
user:onyourwavetopMood Mix and Activity Mix use pre-validated tags selected outside the cached method to ensure
random.choiceactually rotates on each cache miss.My Wave Improvements
track_id@station_iditem IDs for rotor feedbackradioStarted,trackStarted,trackFinished,skipasyncio.Lock)MY_WAVE_BATCH_SIZEcontrols the number of API batches to fetch (not per-batch size)Liked Tracks
Similar Tracks
Uses Yandex Rotor station
track:{id}to get similar tracks for MA radio mode.Configuration
8 settings total (4 shown by default, 4 advanced):
https://api.music.yandex.netArchitecture
New Files
streaming.pyYandexMusicStreamingManager— quality selection, AES-256-CTR decryption, streaming modes, sharedaiohttp.ClientSessionlifecycle, temp file managementconstants.pyTAG_SLUG_CATEGORYmapping,TAG_CATEGORY_ORDERModified Files
__init__.pySUPPORTED_FEATURESincludingLYRICS,BROWSE,RECOMMENDATIONS,SIMILAR_TRACKS; config entriesmanifest.jsonpycryptodome==3.23.0dependencyapi_client.pyYandexMusicClientwrapper with retry logic, rotor station API,/get-file-infolossless endpoint (with_call_with_retry), Landing tags API (with/post/URL filtering), lyrics API (from already-fetched track object), library edit methodsprovider.pyon_played/on_streamed, locale-aware names (RU/EN),is_playable=Falsefor navigation foldersparsers.pyparse_trackaccepts optionallyrics/lyrics_syncedparamsData Flow
Reliability
asyncio.Lockprotects_my_wave_seen_track_idsacross concurrentbrowse/playlist/recommendationscallsget_track()normalizes composite IDs (track_id@station_id→track_id) before caching to prevent duplicate cache entries_call_with_retryhandlesServer disconnected/ network errors (includes lossless API)aiohttp.ClientSessionreused across streams (created lazily, closed onunload)unloadtemp_fd = -1sentinel pattern prevents double-close and fd leaks on HTTP failuressuppress(CancelledError)%sformatting throughoutTests
94 tests across 4 files:
test_integration.pytest_streaming.pytest_recommendations.pytest_api_client.pyconftest.py(parsers)All tests pass. Pre-commit (ruff lint + format) passes.
Localization
Browse folder names adapt to MA locale:
ru_*→ Russian names (Моя волна, Мне нравится, Подборки, etc.)🤖 Generated with Claude Code