From: Eric Munson Date: Sat, 14 Jun 2025 14:01:25 +0000 (-0400) Subject: Feat: Subsonic: Add configurable recommendations (#2226) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=912dd064d7717e4ef50b581696d97ce7cd30ba51;p=music-assistant-server.git Feat: Subsonic: Add configurable recommendations (#2226) The provider has the ability to ask for things like newest albums, most played albums, and anything that is starred by a user. These could be used to fill out the recommendation rows in the home page if the user wants. Add configuration for what kinds (if any) of these recommendations are desired and the size of the list. Signed-off-by: Eric B Munson --- diff --git a/music_assistant/providers/opensubsonic/__init__.py b/music_assistant/providers/opensubsonic/__init__.py index 12ec2806..faa4f362 100644 --- a/music_assistant/providers/opensubsonic/__init__.py +++ b/music_assistant/providers/opensubsonic/__init__.py @@ -13,7 +13,11 @@ from .sonic_provider import ( CONF_BASE_URL, CONF_ENABLE_LEGACY_AUTH, CONF_ENABLE_PODCASTS, + CONF_NEW_ALBUMS, CONF_OVERRIDE_OFFSET, + CONF_PLAYED_ALBUMS, + CONF_RECO_FAVES, + CONF_RECO_SIZE, OpenSonicProvider, ) @@ -86,7 +90,7 @@ async def get_config_entries( ConfigEntry( key=CONF_ENABLE_LEGACY_AUTH, type=ConfigEntryType.BOOLEAN, - label="Enable legacy auth", + label="Enable Legacy Auth", required=True, description='Enable OpenSubsonic "legacy" auth support', default_value=False, @@ -94,10 +98,42 @@ async def get_config_entries( ConfigEntry( key=CONF_OVERRIDE_OFFSET, type=ConfigEntryType.BOOLEAN, - label="Force player provider seek", + label="Force Player Provider Seek", required=True, description="Some Subsonic implementations advertise that they support seeking when " "they do not always. If seeking does not work for you, enable this.", default_value=False, ), + ConfigEntry( + key=CONF_RECO_FAVES, + type=ConfigEntryType.BOOLEAN, + label="Recommend Favorites", + required=True, + description="Should favorited (starred) items be included as recommendations.", + default_value=True, + ), + ConfigEntry( + key=CONF_NEW_ALBUMS, + type=ConfigEntryType.BOOLEAN, + label="Recommend New Albums", + required=True, + description="Should new albums be included as recommendations.", + default_value=True, + ), + ConfigEntry( + key=CONF_PLAYED_ALBUMS, + type=ConfigEntryType.BOOLEAN, + label="Recommend Most Played", + required=True, + description="Should most played albums be included as recommendations.", + default_value=True, + ), + ConfigEntry( + key=CONF_RECO_SIZE, + type=ConfigEntryType.INTEGER, + label="Recommendation Limit", + required=True, + description="How many recommendations from each enabled type should be included.", + default_value=10, + ), ) diff --git a/music_assistant/providers/opensubsonic/sonic_provider.py b/music_assistant/providers/opensubsonic/sonic_provider.py index d4cef92a..1ffb9a2d 100644 --- a/music_assistant/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -30,6 +30,7 @@ from music_assistant_models.media_items import ( Podcast, PodcastEpisode, ProviderMapping, + RecommendationFolder, SearchResults, Track, ) @@ -72,6 +73,10 @@ CONF_BASE_URL = "baseURL" CONF_ENABLE_PODCASTS = "enable_podcasts" CONF_ENABLE_LEGACY_AUTH = "enable_legacy_auth" CONF_OVERRIDE_OFFSET = "override_transcode_offest" +CONF_RECO_FAVES = "recommend_favorites" +CONF_NEW_ALBUMS = "recommend_new" +CONF_PLAYED_ALBUMS = "recommend_played" +CONF_RECO_SIZE = "recommendation_count" Param = ParamSpec("Param") @@ -85,6 +90,10 @@ class OpenSonicProvider(MusicProvider): _enable_podcasts: bool = True _seek_support: bool = False _ignore_offset: bool = False + _show_faves: bool = True + _show_new: bool = True + _show_played: bool = True + _reco_limit: int = 10 async def handle_async_init(self) -> None: """Set up the music provider and test the connection.""" @@ -123,6 +132,10 @@ class OpenSonicProvider(MusicProvider): break except OSError: self.logger.info("Server does not support transcodeOffset, seeking in player provider") + self._show_faves = bool(self.config.get_value(CONF_RECO_FAVES)) + self._show_new = bool(self.config.get_value(CONF_NEW_ALBUMS)) + self._show_played = bool(self.config.get_value(CONF_PLAYED_ALBUMS)) + self._reco_limit = int(str(self.config.get_value(CONF_RECO_SIZE))) @property def supported_features(self) -> set[ProviderFeature]: @@ -135,6 +148,7 @@ class OpenSonicProvider(MusicProvider): ProviderFeature.LIBRARY_PLAYLISTS_EDIT, ProviderFeature.BROWSE, ProviderFeature.SEARCH, + ProviderFeature.RECOMMENDATIONS, ProviderFeature.ARTIST_ALBUMS, ProviderFeature.ARTIST_TOPTRACKS, ProviderFeature.SIMILAR_TRACKS, @@ -760,3 +774,53 @@ class OpenSonicProvider(MusicProvider): streamer_task.cancel() self.logger.debug("Done streaming %s", streamdetails.item_id) + + async def recommendations(self) -> list[RecommendationFolder]: + """Provide recommendations. + + These can provide favorited items, recently added albums, and most played albums. + What is included is configured with the provider. + """ + recos: list[RecommendationFolder] = [] + if self._show_faves: + faves: RecommendationFolder = RecommendationFolder( + item_id="subsonic_starred_albums", provider=self.domain, name="Starred Items" + ) + starred = await self._run_async(self.conn.get_starred2) + if starred.album: + for sonic_album in starred.album[: self._reco_limit]: + faves.items.append(parse_album(self.logger, self.instance_id, sonic_album)) + if starred.artist: + for sonic_artist in starred.artist[: self._reco_limit]: + faves.items.append(parse_artist(self.instance_id, sonic_artist)) + if starred.song: + for sonic_song in starred.song[: self._reco_limit]: + faves.items.append(parse_track(self.logger, self.instance_id, sonic_song)) + + recos.append(faves) + + if self._show_new: + new_stuff: RecommendationFolder = RecommendationFolder( + item_id="subsonic_new_albums", provider=self.domain, name="New Albums" + ) + new_albums = await self._run_async( + self.conn.get_album_list2, ltype="newest", size=self._reco_limit + ) + for sonic_album in new_albums: + new_stuff.items.append(parse_album(self.logger, self.instance_id, sonic_album)) + + recos.append(new_stuff) + + if self._show_played: + recent: RecommendationFolder = RecommendationFolder( + item_id="subsonic_most_played", provider=self.domain, name="Most Played Albums" + ) + albums = await self._run_async( + self.conn.get_album_list2, ltype="frequent", size=self._reco_limit + ) + for sonic_album in albums: + recent.items.append(parse_album(self.logger, self.instance_id, sonic_album)) + + recos.append(recent) + + return recos