/
/
/
## What does this implement/fix? `OpenSonicProvider.get_playlist_tracks` enriches every entry inside the enumeration loop: it fetches the full album (`getAlbum` + `getAlbumInfo2`) per distinct album and the lyrics (`getLyricsBySongId`) per track. Playlist tracks are always resolved from the provider (they are not stored in the DB), so starting or browsing a large playlist fans a single `getPlaylist` response out into thousands of additional, serial Subsonic requests. None of that data is needed to enqueue a track: - `parse_track` already builds the album reference (as an `ItemMapping`) from the playlist entry when no album is passed, which is enough to enqueue and display. - Lyrics are fetched on demand when a track is actually played, via `get_track`. This changes the loop to parse each entry directly (keeping the local `_set_loudness` step), so resolving a playlist is a single `getPlaylist` call. The resolution happens in the shared queue-build / browse layer (`player_queues` and the `music/playlists/playlist_tracks` API command), so this is independent of any player or output: it benefits every player equally and the plain "view a playlist's tracks" browse path as well. ### Measured impact Against a real OpenSubsonic server (Navidrome, ~9,200 tracks), resolving one 718-track playlist (spanning 464 distinct albums): | | Subsonic requests (per cold resolve) | Wall time | |---|---|---| | **Before** | ~1,647 serial: 718 `getLyricsBySongId` + 464 `getAlbum` + 464 `getAlbumInfo2` + 1 `getPlaylist` | **~129 s** | | **After** | **1** `getPlaylist` | **~0.96 s** | About a **134x** reduction in time to first playable track (~99.3%), and it scales with playlist size rather than exploding with it. The wall time before is dominated by serial round-trips (~109 ms each over the network); the fix removes the amplification entirely rather than parallelising it, so it does not trade the latency for a burst of concurrent load on the music server. (The "after" figure was measured through the `music/playlists/playlist_tracks` browse path, i.e. with no player involved at all.) ### Scenarios this improves - **Starting or shuffling a large playlist.** Pressing play on a 700-track playlist previously took roughly two minutes before the first track began; it now starts in about a second. The delay grew with playlist size, so the largest playlists (the ones most people reach for when they just want to shuffle) were hit hardest. - **Browsing a large playlist.** `get_playlist_tracks` also backs the playlist track listing, so opening a big playlist to view or scroll its tracks showed the same multi-minute stall. It now populates immediately. - **Voice intents and automations with a response window.** A "play <playlist>" voice command or a scheduled automation that expects playback to start promptly could exceed its timeout during the multi-minute resolve and surface as a failure, even though the server was still working. Immediate resolution removes that class of false failure. - **Remote or modest music servers.** The old path was ~1,650 serial round-trips, so it hurt most where each round-trip is expensive (a server reached over a WAN/VPN) or where the server is small (a Pi/NAS), or where `getAlbumInfo2` proxies to an external metadata service that then rate-limits the burst. A single `getPlaylist` call is trivial for all of them. ### Considerations / trade-offs - **Album metadata on queue items.** The only field the full `Album` object added over the inline `ItemMapping` is the album `version` string (`parse_track`, where `isinstance(album, Album) and album.version`); tracks now derive their version from the title instead. Full album metadata is still resolved when a track is played (`get_track`), so the now-playing view is unaffected. - **Lyrics.** Unchanged for the now-playing track (fetched via `get_track` at playback). Fetching lyrics for every track only to build a queue was pure overhead. - **Consistency.** This aligns playlist listing with MA's lazy-resolution model: lightweight items in lists, full resolution on play. - **Caching unchanged.** The method keeps its `@use_cache(3h)`; only the cache-miss path changes, so the ~0.96 s above is the cold-resolve time and warm resolves remain instant. - **Scope.** The same per-track enrichment pattern also exists in `get_album_tracks`, `get_artist_toptracks`, and `get_similar_tracks`. This PR is intentionally scoped to the playlist path (the hot spot); happy to follow up on the others in a separate PR if you'd like. **Related issue (if applicable):** - none yet (happy to open one to track it if preferred) ## Types of changes - [x] Bugfix (non-breaking change which fixes an issue) — `bugfix` Present in `stable` as well, so this may be a candidate for the `backport-to-stable` label. ## Checklist - [x] The code change is tested and works locally. (opensubsonic suite: 16 passed on py3.14, plus the live-server measurement above) - [x] `pre-commit run --all-files` passes. (ruff check, ruff format, and mypy all pass) - [x] `pytest` passes, and tests have been added/updated under `tests/` where applicable. (added a test asserting one `getPlaylist` and no per-track album/lyrics fetches) - [ ] For changes to shared models, the companion PR in `music-assistant/models` is linked. (n/a) - [ ] For changes affecting the UI, the companion PR in `music-assistant/frontend` is linked. (n/a) - [x] I have read and complied with the project's AI Policy for any AI-assisted contributions. - [ ] I have raised a PR against the documentation repository targeting the main or beta branch as appropriate. (n/a — no docs change) --------- Co-authored-by: Grayson Adams <[email protected]> Co-authored-by: OzGav <[email protected]> Co-authored-by: Marvin Schenkel <[email protected]>