"[github-actions-workflow]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
- "python.analysis.extraPaths": ["../aiosonos/"]
+ "python.analysis.extraPaths": ["../models/"],
+ "python.analysis.packageIndexDepths": [
+ {
+ "name": "music_assistant_models",
+ "depth": 2
+ }
+ ]
}
ItemMapping,
MediaItemType,
MediaItemTypeOrItemMapping,
+ RecommendationFolder,
SearchResults,
)
from music_assistant_models.provider import SyncTask
provider_instance_id_or_domain=provider_instance_id_or_domain,
)
+ @api_command("music/recommendations")
+ async def recommendations(self) -> list[RecommendationFolder]:
+ """Get all recommendations."""
+ recommendation_providers = [
+ x for x in self.providers if ProviderFeature.RECOMMENDATIONS in x.supported_features
+ ]
+ results_per_provider: list[list[RecommendationFolder]] = await asyncio.gather(
+ self._get_default_recommendations(),
+ *[
+ provider_instance.recommendations()
+ for provider_instance in recommendation_providers
+ ],
+ )
+ # return result from all providers while keeping index
+ # so the result is sorted as each provider delivered
+ return [item for sublist in zip_longest(*results_per_provider) for item in sublist]
+
+ async def _get_default_recommendations(self) -> list[RecommendationFolder]:
+ """Return default recommendations."""
+ return [
+ RecommendationFolder(
+ item_id="in_progress",
+ provider="library",
+ name="In progress",
+ translation_key="in_progress_items",
+ icon="mdi-motion-play",
+ items=await self.in_progress_items(limit=10),
+ ),
+ RecommendationFolder(
+ item_id="recently_played",
+ provider="library",
+ name="Recently played",
+ translation_key="recently_played",
+ icon="mdi-motion-play",
+ items=await self.recently_played(limit=10),
+ ),
+ RecommendationFolder(
+ item_id="random_artists",
+ provider="library",
+ name="Random artists",
+ translation_key="random_artists",
+ icon="mdi-account-music",
+ items=await self.artists.library_items(limit=10, order_by="random"),
+ ),
+ RecommendationFolder(
+ item_id="random_albums",
+ provider="library",
+ name="Random albums",
+ translation_key="random_albums",
+ icon="mdi-album",
+ items=await self.albums.library_items(limit=10, order_by="random"),
+ ),
+ RecommendationFolder(
+ item_id="random_tracks",
+ provider="library",
+ name="Random tracks",
+ translation_key="random_tracks",
+ icon="mdi-file-music",
+ items=await self.tracks.library_items(limit=10, order_by="random"),
+ ),
+ RecommendationFolder(
+ item_id="random_playlists",
+ provider="library",
+ name="Random playlists",
+ translation_key="random_playlists",
+ icon="mdi-playlist-music",
+ items=await self.playlists.library_items(limit=10, order_by="random"),
+ ),
+ ]
+
@api_command("music/item")
async def get_item(
self,
Podcast,
PodcastEpisode,
Radio,
+ RecommendationFolder,
SearchResults,
Track,
)
item_id="artists",
provider=self.instance_id,
path=path + "artists",
- name="",
- label="artists",
+ name="Artists",
+ translation_key="artists",
is_playable=True,
)
)
item_id="albums",
provider=self.instance_id,
path=path + "albums",
- name="",
- label="albums",
+ name="Albums",
+ translation_key="albums",
is_playable=True,
)
)
item_id="tracks",
provider=self.domain,
path=path + "tracks",
- name="",
- label="tracks",
+ name="Tracks",
+ translation_key="tracks",
is_playable=True,
)
)
item_id="playlists",
provider=self.instance_id,
path=path + "playlists",
- name="",
- label="playlists",
+ name="Playlists",
+ translation_key="playlists",
is_playable=True,
)
)
item_id="radios",
provider=self.instance_id,
path=path + "radios",
- name="",
- label="radios",
+ name="Radio",
+ translation_key="radios",
)
)
if ProviderFeature.LIBRARY_AUDIOBOOKS in self.supported_features:
item_id="audiobooks",
provider=self.instance_id,
path=path + "audiobooks",
- name="",
- label="audiobooks",
+ name="Audiobooks",
+ translation_key="audiobooks",
)
)
if ProviderFeature.LIBRARY_PODCASTS in self.supported_features:
item_id="podcasts",
provider=self.instance_id,
path=path + "podcasts",
- name="",
- label="podcasts",
+ name="Podcasts",
+ translation_key="podcasts",
)
)
if len(items) == 1:
return await self.browse(items[0].path)
return items
- async def recommendations(self) -> list[MediaItemType]:
- """Get this provider's recommendations.
+ async def recommendations(self) -> list[RecommendationFolder]:
+ """
+ Get this provider's recommendations.
- Returns a actual and personalised list of Media items with recommendations
- form this provider for the user/account. It may return nested levels with
- BrowseFolder items.
+ Returns an actual (and often personalised) list of recommendations
+ from this provider for the user/account.
"""
if ProviderFeature.RECOMMENDATIONS in self.supported_features:
raise NotImplementedError
Playlist,
ProviderMapping,
Radio,
+ RecommendationFolder,
SearchResults,
Track,
)
# that will call the get_library_* methods if you did not override it.
return []
- async def recommendations(self) -> list[MediaItemType]:
- """Get this provider's recommendations.
+ async def recommendations(self) -> list[RecommendationFolder]:
+ """
+ Get this provider's recommendations.
- Returns a actual and personalised list of Media items with recommendations
- form this provider for the user/account. It may return nested levels with
- BrowseFolder items.
+ Returns an actual (and often personalised) list of recommendations
+ from this provider for the user/account.
"""
# Get this provider's recommendations.
# This is only called if you reported the RECOMMENDATIONS feature in the supported_features.
MediaItemType,
Playlist,
ProviderMapping,
+ RecommendationFolder,
SearchResults,
Track,
)
raise NotImplementedError
return result
- async def recommendations(self) -> list[Track]:
+ async def recommendations(self) -> list[RecommendationFolder]:
"""Get deezer's recommendations."""
return [
- self.parse_track(track=track, user_country=self.gw_client.user_country)
- for track in await self.client.get_user_recommended_tracks()
+ RecommendationFolder(
+ item_id="recommended_tracks",
+ name="Recommended tracks",
+ translation_key="recommended_tracks",
+ items=[
+ self.parse_track(track=track, user_country=self.gw_client.user_country)
+ for track in await self.client.get_user_recommended_tracks()
+ ],
+ )
]
async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None:
provider=self.domain,
path=path + "popular",
name="",
- label="radiobrowser_by_popularity",
+ translation_key="radiobrowser_by_popularity",
),
BrowseFolder(
item_id="country",
provider=self.domain,
path=path + "country",
name="",
- label="radiobrowser_by_country",
+ translation_key="radiobrowser_by_country",
),
BrowseFolder(
item_id="tag",
provider=self.domain,
path=path + "tag",
name="",
- label="radiobrowser_by_tag",
+ translation_key="radiobrowser_by_tag",
),
]
path=base_path + "/" + country.code.lower(),
name=country.name,
)
- folder.metadata.images = UniqueList(
- [
- MediaItemImage(
- type=ImageType.THUMB,
- path=country.favicon,
- provider=self.lookup_key,
- remotely_accessible=True,
- )
- ]
+ folder.image = MediaItemImage(
+ type=ImageType.THUMB,
+ path=country.favicon,
+ provider=self.lookup_key,
+ remotely_accessible=True,
)
items.append(folder)
return items
"ifaddr==0.2.0",
"mashumaro==3.15",
"music-assistant-frontend==2.13.1",
- "music-assistant-models==1.1.36",
+ "music-assistant-models==1.1.37",
"mutagen==1.47.0",
"orjson==3.10.15",
"pillow==11.1.0",
liblistenbrainz==0.5.6
mashumaro==3.15
music-assistant-frontend==2.13.1
-music-assistant-models==1.1.36
+music-assistant-models==1.1.37
mutagen==1.47.0
orjson==3.10.15
pillow==11.1.0
'name': 'Papa Roach',
'provider': 'xx-instance-id-xx',
'sort_name': 'papa roach',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/e439648e08ade14e27d5de48fa97c88e',
'version': '',
}),
}),
]),
'sort_name': 'infest',
+ 'translation_key': None,
'uri': 'jellyfin://album/70b7288088b42d318f75dbcc41fd0091',
'version': '',
'year': 2000,
'name': 'Emmy the Great & Tim Wheeler',
'provider': 'xx-instance-id-xx',
'sort_name': 'emmy the great & tim wheeler',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/555b36f7d310d1b7405557a8775c6878',
'version': '',
}),
}),
]),
'sort_name': 'this is christmas',
+ 'translation_key': None,
'uri': 'jellyfin://album/32ed6a0091733dcff57eae67010f3d4b',
'version': '',
'year': 2011,
'name': '[unknown]',
'provider': 'jellyfin',
'sort_name': 'unknown]',
+ 'translation_key': None,
'uri': 'jellyfin://artist/[unknown]',
'version': '',
}),
}),
]),
'sort_name': 'yesterday when i was mad [disc 0000000002]',
+ 'translation_key': None,
'uri': 'jellyfin://album/7c8d0bd55291c7fc0451d17ebef30017',
'version': '',
'year': None,
}),
]),
'sort_name': 'ash',
+ 'translation_key': None,
'uri': 'jellyfin://artist/dd954bbf54398e247d803186d3585b79',
'version': '',
})
'name': 'AM',
'provider': 'xx-instance-id-xx',
'sort_name': 'am',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://album/d42d74e134693184e7adc73106238e89',
'version': '',
}),
'name': 'Arctic Monkeys',
'provider': 'xx-instance-id-xx',
'sort_name': 'arctic monkeys',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/cc940aeb8a99149f159fe9794f136071',
'version': '',
}),
]),
'sort_name': 'do i wanna know?',
'track_number': 1,
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://track/da9c458e425584680765ddc3a89cbc0c',
'version': '',
})
'name': 'Unknown Album (70b7288088b42d318f75dbcc41fd0091)',
'provider': 'xx-instance-id-xx',
'sort_name': 'unknown album (70b7288088b42d318f75dbcc41fd0091)',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://album/70b7288088b42d318f75dbcc41fd0091',
'version': '',
}),
'name': '[unknown]',
'provider': 'jellyfin',
'sort_name': 'unknown]',
+ 'translation_key': None,
'uri': 'jellyfin://artist/[unknown]',
'version': '',
}),
]),
'sort_name': '11 thrown away',
'track_number': 0,
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://track/b5319fb11cde39fca2023184fcfa9862',
'version': '',
})
'name': 'Dead Like Harry',
'provider': 'xx-instance-id-xx',
'sort_name': 'dead like harry',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/94875b0dd58cbf5245a135982133651a',
'version': '',
}),
]),
'sort_name': 'where the bands are (2018 version)',
'track_number': 1,
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://track/54918f75ee8f6c8b8dc5efd680644f29',
'version': '',
})
'name': 'This Is Christmas',
'provider': 'xx-instance-id-xx',
'sort_name': 'this is christmas',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://album/32ed6a0091733dcff57eae67010f3d4b',
'version': '',
}),
'name': 'Emmy the Great',
'provider': 'xx-instance-id-xx',
'sort_name': 'emmy the great',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/a0c459294295710546c81c20a8d9abfc',
'version': '',
}),
'name': 'Tim Wheeler',
'provider': 'xx-instance-id-xx',
'sort_name': 'tim wheeler',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/1952db245ddef4e41dcd016475379190',
'version': '',
}),
]),
'sort_name': 'zombie christmas',
'track_number': 8,
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://track/fb12a77f49616a9fc56a6fab2b94174c',
'version': '',
})
'name': 'pornophonique',
'provider': 'xx-instance-id-xx',
'sort_name': 'pornophonique',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/91c3901ac465b9efc439e4be4270c2b6',
'version': '',
}),
}),
]),
'sort_name': '8-bit lagerfeuer',
+ 'translation_key': None,
'uri': 'opensubsonic://album/ad0f112b6dcf83de5e9cae85d07f0d35',
'version': '',
'year': 2007,
'name': 'pornophonique',
'provider': 'xx-instance-id-xx',
'sort_name': 'pornophonique',
+ 'translation_key': None,
'uri': 'xx-instance-id-xx://artist/91c3901ac465b9efc439e4be4270c2b6',
'version': '',
}),
}),
]),
'sort_name': '8-bit lagerfeuer',
+ 'translation_key': None,
'uri': 'opensubsonic://album/ad0f112b6dcf83de5e9cae85d07f0d35',
'version': '',
'year': 2007,
}),
]),
'sort_name': '2 mello',
+ 'translation_key': None,
'uri': 'opensubsonic://artist/37ec820ca7193e17040c98f7da7c4b51',
'version': '',
})
}),
]),
'sort_name': '2 mello',
+ 'translation_key': None,
'uri': 'opensubsonic://artist/37ec820ca7193e17040c98f7da7c4b51',
'version': '',
})
}),
]),
'sort_name': 'synthetic',
+ 'translation_key': None,
'uri': 'opensubsonic://artist/100000002',
'version': '',
})
}),
]),
'sort_name': 'synthetic',
+ 'translation_key': None,
'uri': 'opensubsonic://artist/100000002',
'version': '',
})