Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 40 additions & 3 deletions music_assistant/providers/smart_playlist/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,29 @@ async def get_recommendation_items(
items: UniqueList[MediaItemType | ItemMapping | BrowseFolder] = UniqueList()
if item_id != "smart_playlists":
return items

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
)
if lib_item:
library_mappings[pid] = str(lib_item.item_id)
except Exception as err:
self.logger.debug("Could not get library item for %s: %s", pid, err)

for pid, rules in self._rules_store.items():
items.append(await self._build_playlist(pid, rules))
playlist = await self._build_playlist(pid, rules)
if pid in library_mappings:
playlist.provider_mappings.add(
ProviderMapping(
item_id=library_mappings[pid],
provider_domain="library",
provider_instance="library",
)
Comment on lines +336 to +341
)
items.append(playlist)
return items

async def get_playlist(self, prov_playlist_id: str) -> Playlist:
Expand All @@ -329,8 +350,24 @@ async def get_playlist(self, prov_playlist_id: str) -> Playlist:
msg = f"Smart playlist {prov_playlist_id} not found"
raise MediaNotFoundError(msg)

# Build playlist from rules
return await self._build_playlist(resolved_id, rules)
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
)
if lib_item:
playlist.provider_mappings.add(
ProviderMapping(
item_id=str(lib_item.item_id),
provider_domain="library",
provider_instance="library",
)
)
except Exception as err:
self.logger.debug("Could not get library item for %s: %s", resolved_id, err)

return playlist

async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]:
"""
Expand Down
45 changes: 43 additions & 2 deletions tests/providers/smart_playlist/test_recommendations.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ async def test_get_recommendation_items_builds_playlists() -> None:
assert [item.item_id for item in result] == ["abc", "def"]
assert all(isinstance(item, Playlist) for item in result)
assert [item.name for item in result] == ["Playlist A", "Playlist B"]
# the per-playlist artwork lookup ran for each built playlist
assert mass.music.playlists.get_library_item_by_prov_id.await_count == 2
# library mapping lookup + artwork lookup for each playlist
assert mass.music.playlists.get_library_item_by_prov_id.await_count == 4


async def test_get_recommendation_items_unknown_id_returns_empty() -> None:
Expand All @@ -77,3 +77,44 @@ async def test_get_recommendation_items_unknown_id_returns_empty() -> None:

assert list(result) == []
mass.music.playlists.get_library_item_by_prov_id.assert_not_awaited()


async def test_get_recommendation_items_adds_library_mappings() -> None:
"""Recommendation items include library provider_mappings for shortcut recognition."""
plugin, mass = _make_plugin()
plugin._rules_store["abc"] = SmartPlaylistRules(limit=10)
plugin._names_store["abc"] = "Test Playlist"

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

result = await plugin.get_recommendation_items("smart_playlists")

assert len(result) == 1
playlist = result[0]
assert isinstance(playlist, Playlist)

library_mappings = [m for m in playlist.provider_mappings if m.provider_domain == "library"]
assert len(library_mappings) == 1
assert library_mappings[0].item_id == "123"
assert library_mappings[0].provider_instance == "library"


async def test_get_playlist_adds_library_mapping() -> None:
"""get_playlist includes library provider_mapping for shortcut recognition."""
plugin, mass = _make_plugin()
plugin._rules_store["abc"] = SmartPlaylistRules(limit=10)
plugin._names_store["abc"] = "Test Playlist"

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

result = await plugin.get_playlist("abc")

assert isinstance(result, Playlist)
library_mappings = [m for m in result.provider_mappings if m.provider_domain == "library"]
assert len(library_mappings) == 1
assert library_mappings[0].item_id == "456"
assert library_mappings[0].provider_instance == "library"
7 changes: 3 additions & 4 deletions tests/providers/smart_playlist/test_smart_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,10 +1277,9 @@ async def test_get_playlist_loads_library_artwork(
assert playlist.metadata.images[0].path == "generated_artwork.jpg"
assert playlist.metadata.images[0].provider == "playlist_art"

# Verify library lookup was called
mass.music.playlists.get_library_item_by_prov_id.assert_awaited_once_with(
"abc", plugin.instance_id
)
# Library lookup called twice: once for provider_mapping, once for artwork
assert mass.music.playlists.get_library_item_by_prov_id.await_count == 2
mass.music.playlists.get_library_item_by_prov_id.assert_awaited_with("abc", plugin.instance_id)


@pytest.mark.asyncio
Expand Down