Skip to content

Fix smart playlist shortcut recognition from recommendation rows - #5567

Closed
dmoo500 wants to merge 2 commits into
music-assistant:devfrom
dmoo500:bugfix/smart-playlist-shortcut-recognition
Closed

dmoo500 wants to merge 2 commits into
music-assistant:devfrom
dmoo500:bugfix/smart-playlist-shortcut-recognition

Conversation

@dmoo500

@dmoo500 dmoo500 commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

What does this implement/fix?

Fixes smart playlists pinned from recommendation rows not appearing in the sidebar. The issue was introduced in #4487 when recommendation loading was changed to on-demand.

When a user pins a smart playlist from a recommendation row, the frontend saves a URI like smart_playlist--1://playlist/abc. When the shortcut system loads this URI via getItemByUri(), the server returns a playlist without library provider_mappings. This prevents the frontend from matching it with already-pinned library versions, causing the shortcut to not appear in the sidebar even though it's correctly saved in preferences.

Solution:
Add library provider_mappings to smart playlists when loaded via:

  • get_recommendation_items() - for recommendation row items (batch lookup for performance)
  • get_playlist() - when loading by URI

This enables frontend identity matching to work correctly.

Related issue (if applicable):

  • Reported by user in nightly testing

Types of changes

  • Bugfix (non-breaking change which fixes an issue) — bugfix
  • New feature (non-breaking change which adds functionality) — new-feature
  • Enhancement to an existing feature — enhancement
  • New music/player/metadata/plugin provider — new-provider
  • Breaking change (fix or feature that would cause existing functionality to not work as expected) — breaking-change
  • Refactor (no behaviour change) — refactor
  • Documentation only — documentation
  • Maintenance / chore — maintenance
  • CI / workflow change — ci
  • Dependencies bump — dependencies

Checklist

  • The code change is tested and works locally.
  • pre-commit run --all-files passes.
  • pytest passes, and tests have been added/updated under tests/ where applicable.
  • For changes to shared models, the companion PR in music-assistant/models is linked.
  • For changes affecting the UI, the companion PR in music-assistant/frontend is linked: music-assistant/frontend#[PR_NUMBER]
  • 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.

Add library provider_mappings to smart playlists when loaded via
get_recommendation_items() and get_playlist(). This enables the
frontend to correctly match smart playlists pinned from recommendation
rows with their library equivalents, fixing an issue where these
shortcuts would not appear in the sidebar.

The issue was introduced in music-assistant#4487 when recommendation loading was
changed to on-demand - playlists from get_recommendation_items()
never had library mappings, preventing identity matching.
Copilot AI lite review requested due to automatic review settings August 10, 2026 18:43
@musicassistant-bot

musicassistant-bot Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

✅ The title and description are good to go. Thanks!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to fix smart playlists pinned from recommendation rows not appearing in the frontend sidebar by ensuring smart-playlist items can be correlated with their library counterparts during URI-based lookups.

Changes:

  • Adds logic in get_recommendation_items() to look up a corresponding library playlist for each smart playlist.
  • Adds similar library lookup logic in get_playlist() when resolving a smart playlist by provider ID/URI.
Suppressed comments (1)

music_assistant/providers/smart_playlist/init.py:366

  • [CRITICAL] Same issue here: injecting a ProviderMapping with provider_instance='library'/provider_domain='library' into provider_mappings can later trip assertions in library-matching code that expects provider mappings to always reference real providers. Returning the existing library playlist (when present) avoids corrupting provider_mappings and still yields the correct identity for the client.
        try:
            lib_item = await self.mass.music.playlists.get_library_item_by_prov_id(
                resolved_id, self.instance_id
            )
            if lib_item:
                playlist.provider_mappings.add(
                    ProviderMapping(
                        item_id=str(lib_item.item_id),
                        provider_domain="library",
                        provider_instance="library",
                    )
                )

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +336 to +341
playlist.provider_mappings.add(
ProviderMapping(
item_id=library_mappings[pid],
provider_domain="library",
provider_instance="library",
)
Copilot AI review requested due to automatic review settings August 10, 2026 18:56
@dmoo500
dmoo500 marked this pull request as draft August 10, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (4)

music_assistant/providers/smart_playlist/init.py:358

  • [PROBLEM] get_playlist now always performs two library lookups (one inside _build_playlist via _images_for, then another here to add the library ProviderMapping), doubling DB/API work on a hot path. Consider fetching the library item once and reusing it for both images and the mapping (e.g., pass a prefetched library item into _build_playlist/_images_for or add a small in-method cache).
        playlist = await self._build_playlist(resolved_id, rules)

        try:
            lib_item = await self.mass.music.playlists.get_library_item_by_prov_id(
                resolved_id, self.instance_id
            )

tests/providers/smart_playlist/test_recommendations.py:112

  • [PROBLEM] This mock returns a truthy metadata.images MagicMock by default, so _images_for() may treat it as artwork and return a MagicMock instead of an empty list, making the test less representative and potentially fragile. Set metadata.images to an empty list explicitly.
    lib_playlist = MagicMock()
    lib_playlist.item_id = 456
    mass.music.playlists.get_library_item_by_prov_id = AsyncMock(return_value=lib_playlist)

music_assistant/providers/smart_playlist/init.py:326

  • [PROBLEM] get_recommendation_items iterates directly over self._rules_store while awaiting I/O, which can raise RuntimeError: dictionary changed size during iteration if rules are added/removed concurrently; it also performs the library lookups sequentially, increasing worst-case latency for rows with many playlists. Snapshot the items first and run the library lookups concurrently (with return_exceptions=True) before building playlists.
        library_mappings: dict[str, str] = {}
        for pid in self._rules_store:
            try:
                lib_item = await self.mass.music.playlists.get_library_item_by_prov_id(
                    pid, self.instance_id

tests/providers/smart_playlist/test_recommendations.py:91

  • [PROBLEM] This mock returns a truthy metadata.images MagicMock by default, so _images_for() may treat it as artwork and return a MagicMock instead of an empty list, making the test less representative and potentially fragile. Set metadata.images to an empty list explicitly.

This issue also appears on line 110 of the same file.

    lib_playlist = MagicMock()
    lib_playlist.item_id = 123
    mass.music.playlists.get_library_item_by_prov_id = AsyncMock(return_value=lib_playlist)

@dmoo500 dmoo500 closed this Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants