/
/
1"""
2Builtin Library Recommendations Provider.
3
4Surfaces library-based discovery rows as recommendations on the Discover page.
5"""
6
7from __future__ import annotations
8
9from enum import StrEnum
10from typing import TYPE_CHECKING
11
12from music_assistant_models.enums import MediaType, ProviderFeature
13from music_assistant_models.media_items import RecommendationFolder, UniqueList
14
15from music_assistant.models.plugin import PluginProvider
16
17if TYPE_CHECKING:
18 from collections.abc import Sequence
19
20 from music_assistant_models.config_entries import (
21 ConfigEntry,
22 ConfigValueType,
23 ProviderConfig,
24 )
25 from music_assistant_models.media_items import BrowseFolder, ItemMapping, MediaItemType
26 from music_assistant_models.provider import ProviderManifest
27
28 from music_assistant.mass import MusicAssistant
29 from music_assistant.models import ProviderInstanceType
30
31SUPPORTED_FEATURES: set[ProviderFeature] = {
32 ProviderFeature.RECOMMENDATIONS,
33}
34
35
36class LibraryRowID(StrEnum):
37 """The item_ids of library recommendation rows."""
38
39 IN_PROGRESS = "in_progress"
40 RECENTLY_PLAYED = "recently_played"
41 RECENTLY_ADDED_TRACKS = "recently_added_tracks"
42 RECENTLY_ADDED_ALBUMS = "recently_added_albums"
43 RANDOM_ARTISTS = "random_artists"
44 RANDOM_ALBUMS = "random_albums"
45 RECENT_FAVORITE_TRACKS = "recent_favorite_tracks"
46 FAVORITE_PLAYLISTS = "favorite_playlists"
47 FAVORITE_RADIO = "favorite_radio"
48 RECENT_ARTISTS = "recent_artists"
49 RECENT_TRACKS = "recent_tracks"
50 FORGOTTEN_TRACKS = "forgotten_tracks"
51 FORGOTTEN_ALBUMS = "forgotten_albums"
52 FORGOTTEN_ARTISTS = "forgotten_artists"
53 MOST_PLAYED_TRACKS = "most_played_tracks"
54 NEVER_PLAYED_TRACKS = "never_played_tracks"
55
56
57async def setup(
58 mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
59) -> ProviderInstanceType:
60 """Initialize provider(instance) with given configuration."""
61 return LibraryRecommendationsProvider(mass, manifest, config, SUPPORTED_FEATURES)
62
63
64async def get_config_entries(
65 mass: MusicAssistant, # noqa: ARG001
66 instance_id: str | None = None, # noqa: ARG001
67 action: str | None = None, # noqa: ARG001
68 values: dict[str, ConfigValueType] | None = None, # noqa: ARG001
69) -> tuple[ConfigEntry, ...]:
70 """Return config entries for this provider."""
71 return ()
72
73
74class LibraryRecommendationsProvider(PluginProvider):
75 """Builtin provider for library-based recommendation rows."""
76
77 async def get_recommendations(self) -> list[RecommendationFolder]:
78 """Get all library recommendation rows, without items."""
79 return [
80 _folder(
81 LibraryRowID.IN_PROGRESS, "In progress", "in_progress_items", "mdi-motion-play"
82 ),
83 _folder(
84 LibraryRowID.RECENTLY_PLAYED,
85 "Recently played",
86 "recently_played",
87 "mdi-motion-play",
88 ),
89 _folder(
90 LibraryRowID.RECENTLY_ADDED_TRACKS,
91 "Recently added tracks",
92 "recently_added_tracks",
93 "mdi-music-note-plus",
94 False,
95 ),
96 _folder(
97 LibraryRowID.RECENTLY_ADDED_ALBUMS,
98 "Recently added albums",
99 "recently_added_albums",
100 "mdi-album",
101 ),
102 _folder(
103 LibraryRowID.RANDOM_ARTISTS,
104 "Random artists",
105 "random_artists",
106 "mdi-account-music",
107 False,
108 ),
109 _folder(
110 LibraryRowID.RANDOM_ALBUMS, "Random albums", "random_albums", "mdi-album", False
111 ),
112 _folder(
113 LibraryRowID.RECENT_FAVORITE_TRACKS,
114 "Recently favorited tracks",
115 "recent_favorite_tracks",
116 "mdi-file-music",
117 False,
118 ),
119 _folder(
120 LibraryRowID.FAVORITE_PLAYLISTS,
121 "Favorite playlists",
122 "favorite_playlists",
123 "mdi-playlist-music",
124 False,
125 ),
126 _folder(
127 LibraryRowID.FAVORITE_RADIO,
128 "Favorite Radio stations",
129 "favorite_radio_stations",
130 "mdi-access-point",
131 False,
132 ),
133 _folder(
134 LibraryRowID.RECENT_ARTISTS,
135 "Recent artists",
136 "recent_artists",
137 "mdi-account-music",
138 False,
139 ),
140 _folder(
141 LibraryRowID.RECENT_TRACKS,
142 "Recent tracks",
143 "recent_tracks",
144 "mdi-music-note",
145 False,
146 ),
147 _folder(
148 LibraryRowID.FORGOTTEN_TRACKS,
149 "Forgotten Tracks",
150 "forgotten_tracks",
151 "mdi-timer-sand",
152 False,
153 ),
154 _folder(
155 LibraryRowID.FORGOTTEN_ALBUMS,
156 "Forgotten Albums",
157 "forgotten_albums",
158 "mdi-timer-sand",
159 False,
160 ),
161 _folder(
162 LibraryRowID.FORGOTTEN_ARTISTS,
163 "Forgotten Artists",
164 "forgotten_artists",
165 "mdi-timer-sand",
166 False,
167 ),
168 _folder(
169 LibraryRowID.MOST_PLAYED_TRACKS,
170 "Most Played Tracks",
171 "most_played_tracks",
172 "mdi-trophy",
173 False,
174 ),
175 _folder(
176 LibraryRowID.NEVER_PLAYED_TRACKS,
177 "Never / Rarely Played",
178 "never_played_tracks",
179 "mdi-sleep",
180 False,
181 ),
182 ]
183
184 async def get_recommendation_items(
185 self, item_id: str
186 ) -> UniqueList[MediaItemType | ItemMapping | BrowseFolder]:
187 """Get the items for a single library recommendation row."""
188 items: Sequence[MediaItemType | ItemMapping | BrowseFolder]
189 match item_id:
190 case LibraryRowID.IN_PROGRESS:
191 items = await self.mass.music.in_progress_items(limit=10)
192 case LibraryRowID.RECENTLY_PLAYED:
193 items = await self.mass.music.recently_played(
194 limit=10,
195 media_types=[
196 MediaType.ALBUM,
197 MediaType.TRACK,
198 MediaType.PLAYLIST,
199 MediaType.ARTIST,
200 MediaType.GENRE,
201 ],
202 user_initiated_only=True,
203 always_include_media_types=[MediaType.PODCAST, MediaType.AUDIOBOOK],
204 )
205 case LibraryRowID.RECENTLY_ADDED_TRACKS:
206 items = await self.mass.music.tracks.library_items(
207 limit=10, order_by="timestamp_added_desc"
208 )
209 case LibraryRowID.RECENTLY_ADDED_ALBUMS:
210 items = await self.mass.music.albums.library_items(
211 limit=10, order_by="timestamp_added_desc"
212 )
213 case LibraryRowID.RANDOM_ARTISTS:
214 items = await self.mass.music.artists.library_items(
215 limit=10, order_by="random_play_count"
216 )
217 case LibraryRowID.RANDOM_ALBUMS:
218 items = await self.mass.music.albums.library_items(
219 limit=10, order_by="random_play_count"
220 )
221 case LibraryRowID.RECENT_FAVORITE_TRACKS:
222 items = await self.mass.music.tracks.library_items(
223 favorite=True, limit=10, order_by="timestamp_modified_desc"
224 )
225 case LibraryRowID.FAVORITE_PLAYLISTS:
226 items = await self.mass.music.playlists.library_items(
227 favorite=True, limit=10, order_by="random"
228 )
229 case LibraryRowID.FAVORITE_RADIO:
230 items = await self.mass.music.radio.library_items(
231 favorite=True, limit=10, order_by="play_count_desc"
232 )
233 case LibraryRowID.RECENT_ARTISTS:
234 items = await self.mass.music.recently_played(
235 limit=10, media_types=[MediaType.ARTIST], user_initiated_only=False
236 )
237 case LibraryRowID.RECENT_TRACKS:
238 items = await self.mass.music.recently_played(
239 limit=10, media_types=[MediaType.TRACK], user_initiated_only=False
240 )
241 case LibraryRowID.FORGOTTEN_TRACKS:
242 items = await self.mass.music.tracks.library_items(
243 limit=10, order_by="last_played", played_only=True
244 )
245 case LibraryRowID.FORGOTTEN_ALBUMS:
246 items = await self.mass.music.albums.library_items(
247 limit=10, order_by="last_played", played_only=True
248 )
249 case LibraryRowID.FORGOTTEN_ARTISTS:
250 items = await self.mass.music.artists.library_items(
251 limit=10, order_by="last_played", played_only=True
252 )
253 case LibraryRowID.MOST_PLAYED_TRACKS:
254 items = await self.mass.music.tracks.library_items(
255 limit=10, order_by="play_count_desc"
256 )
257 case LibraryRowID.NEVER_PLAYED_TRACKS:
258 items = await self.mass.music.tracks.library_items(limit=10, order_by="play_count")
259 case _:
260 items = []
261 return UniqueList(items)
262
263
264def _folder(
265 item_id: LibraryRowID,
266 name: str,
267 translation_key: str,
268 icon: str,
269 enabled_by_default: bool = True,
270) -> RecommendationFolder:
271 """Create a recommendation folder metadata object."""
272 return RecommendationFolder(
273 item_id=item_id.value,
274 provider="recommendations",
275 name=name,
276 translation_key=translation_key,
277 icon=icon,
278 enabled_by_default=enabled_by_default,
279 uri=f"library://folder/{item_id.value}",
280 )
281