/
/
1"""
2DEMO/TEMPLATE Music Provider for Music Assistant.
3
4This is an empty music provider with no actual implementation.
5Its meant to get started developing a new music provider for Music Assistant.
6
7Use it as a reference to discover what methods exists and what they should return.
8Also it is good to look at existing music providers to get a better understanding,
9due to the fact that providers may be flexible and support different features.
10
11If you are relying on a third-party library to interact with the music source,
12you can then reference your library in the manifest in the requirements section,
13which is a list of (versioned!) python modules (pip syntax) that should be installed
14when the provider is selected by the user.
15
16Please keep in mind that Music Assistant is a fully async application and all
17methods should be implemented as async methods. If you are not familiar with
18async programming in Python, we recommend you to read up on it first.
19If you are using a third-party library that is not async, you will need to use the
20helper methods such as asyncio.to_thread or the create_task in the mass object to wrap
21the calls to the library in a thread.
22
23To add a new provider to Music Assistant, you need to create a new folder
24in the providers folder with the name of your provider (e.g. 'my_music_provider').
25In that folder you should create (at least) a __init__.py file and a manifest.json file.
26
27As the provider gets bigger it is preferred to split it up. Start with __init__.py,
28constants.py and provider.py. Other often used files are helpers.py, parsers.py and
29streaming.py
30
31Optional, but strongly desired, are icon.svg and icon_monochrome.svg files that will be used
32as the icon for the provider in the UI, but if this is not possible then we also support
33a material design icon in the manifest.json file.
34
35IMPORTANT NOTE:
36We strongly recommend developing on either macOS or Linux and start your development
37environment by running the setup.sh script in the scripts folder of the repository.
38This will create a virtual environment and install all dependencies needed for development.
39See also our general DEVELOPMENT.md guide in the repository for more information.
40
41"""
42
43from __future__ import annotations
44
45from collections.abc import AsyncGenerator, Sequence
46from datetime import datetime
47from typing import TYPE_CHECKING
48
49from music_assistant_models.enums import ContentType, MediaType, ProviderFeature, StreamType
50from music_assistant_models.media_items import (
51 Album,
52 Artist,
53 AudioFormat,
54 BrowseFolder,
55 ItemMapping,
56 MediaItemType,
57 Playlist,
58 ProviderMapping,
59 Radio,
60 RecommendationFolder,
61 SearchResults,
62 Track,
63 UniqueList,
64)
65from music_assistant_models.streamdetails import StreamDetails
66
67from music_assistant.models.music_provider import MusicProvider
68
69if TYPE_CHECKING:
70 from music_assistant_models.config_entries import (
71 ConfigActionResult,
72 ConfigEntry,
73 ProviderConfig,
74 )
75 from music_assistant_models.provider import ProviderManifest
76
77 from music_assistant.mass import MusicAssistant
78 from music_assistant.models import ProviderInstanceType
79
80
81SUPPORTED_FEATURES = {
82 ProviderFeature.BROWSE,
83 ProviderFeature.SEARCH,
84 ProviderFeature.RECOMMENDATIONS,
85 ProviderFeature.LIBRARY_ARTISTS,
86 ProviderFeature.LIBRARY_ALBUMS,
87 ProviderFeature.LIBRARY_TRACKS,
88 ProviderFeature.LIBRARY_PLAYLISTS,
89 ProviderFeature.ARTIST_ALBUMS,
90 ProviderFeature.ARTIST_TOPTRACKS,
91 ProviderFeature.LIBRARY_ARTISTS_EDIT,
92 ProviderFeature.LIBRARY_ALBUMS_EDIT,
93 ProviderFeature.LIBRARY_TRACKS_EDIT,
94 ProviderFeature.LIBRARY_PLAYLISTS_EDIT,
95 ProviderFeature.SIMILAR_TRACKS,
96 # MANDATORY
97 # this constant should contain a set of provider-level features
98 # that your music provider supports or an empty set if none.
99 # for example 'ProviderFeature.BROWSE' if you can browse the provider's items.
100 # see the ProviderFeature enum for all available features
101}
102
103
104async def setup(
105 mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
106) -> ProviderInstanceType:
107 """Initialize provider(instance) with given configuration."""
108 # setup is called when the user wants to setup a new provider instance.
109 # you are free to do any preflight checks here and but you must return
110 # an instance of the provider.
111 return MyDemoMusicprovider(mass, manifest, config, SUPPORTED_FEATURES)
112
113
114class MyDemoMusicprovider(MusicProvider):
115 """
116 Example/demo Music provider.
117
118 Note that this is always subclassed from MusicProvider,
119 which in turn is a subclass of the generic Provider model.
120
121 The base implementation already takes care of some convenience methods,
122 such as the mass object and the logger. Take a look at the base class
123 for more information on what is available.
124
125 Just like with any other subclass, make sure that if you override
126 any of the default methods (such as __init__), you call the super() method.
127 In most cases its not needed to override any of the builtin methods and you only
128 implement the abc methods with your actual implementation.
129 """
130
131 async def get_config_entries(self) -> tuple[ConfigEntry, ...]:
132 """
133 Return the (options) config entries for this (existing) provider instance.
134
135 This is called only for an already set-up instance to render its options page,
136 so you can read the current values with ``self.get_config_value`` and inspect
137 capabilities with ``self.supported_features``. Return an empty tuple when the
138 provider has no options.
139
140 One-time setup input (credentials, tokens, an OAuth/QR login, picking a device,
141 ...) is NOT collected here - it is collected by the interactive setup flow in
142 ``setup_flow.py`` (see the ``run_setup`` function there). If your provider needs
143 no setup input at all, simply omit ``setup_flow.py``.
144
145 For one-shot buttons (e.g. "clear cache") add a ``ConfigEntryType.ACTION`` entry
146 here and handle its press in ``handle_config_action`` below.
147 """
148 return ()
149
150 async def handle_config_action(
151 self, action: str
152 ) -> tuple[ConfigEntry, ...] | ConfigActionResult | None:
153 """
154 Handle a one-shot ACTION button press from the options page.
155
156 Run the side effect for the pressed ``action`` (the ``action`` id of one of the
157 ``ConfigEntryType.ACTION`` entries returned by ``get_config_entries``) and return
158 None: the action is a one-off, with nothing to re-render. Raise (typically
159 ``ActionUnavailable``) to report that the action could not run. Return config
160 entries only when the options page must re-render with different entries.
161 Delegate unknown actions to ``super()`` (which raises ``ActionUnavailable``).
162
163 Remove this method entirely when your provider declares no ACTION entries.
164 """
165 return await super().handle_config_action(action)
166
167 async def loaded_in_mass(self) -> None:
168 """Call after the provider has been loaded."""
169 # OPTIONAL
170 # this is an optional method that you can implement if
171 # relevant or leave out completely if not needed.
172 # In most cases this can be omitted for music providers.
173
174 async def unload(self, is_removed: bool = False) -> None:
175 """
176 Handle unload/close of the provider.
177
178 Called when provider is deregistered (e.g. MA exiting or config reloading).
179 is_removed will be set to True when the provider is removed from the configuration.
180 """
181 # OPTIONAL
182 # This is an optional method that you can implement if
183 # relevant or leave out completely if not needed.
184 # It will be called when the provider is unloaded from Music Assistant.
185 # for example to disconnect from a service or clean up resources.
186
187 @property
188 def is_streaming_provider(self) -> bool:
189 """
190 Return True if the provider is a streaming provider.
191
192 This literally means that the catalog is not the same as the library contents.
193 For local based providers (files, plex), the catalog is the same as the library content.
194 It also means that data is if this provider is NOT a streaming provider,
195 data cross instances is unique, the catalog and library differs per instance.
196
197 Setting this to True will only query one instance of the provider for search and lookups.
198 Setting this to False will query all instances of this provider for search and lookups.
199 """
200 # For streaming providers return True here but for local file based providers return False.
201 return True
202
203 async def search( # type: ignore[empty-body]
204 self,
205 search_query: str,
206 media_types: list[MediaType],
207 limit: int = 5,
208 ) -> SearchResults:
209 """
210 Perform search on musicprovider.
211
212 :param search_query: Search query.
213 :param media_types: A list of media_types to include.
214 :param limit: Number of items to return in the search (per type).
215 """
216 # OPTIONAL
217 # Will only be called if you reported the SEARCH feature in the supported_features.
218 # It allows searching your provider for media items.
219 # See the model for SearchResults for more information on what to return, but
220 # in general you should return a list of MediaItems for each media type.
221 # For radio, a simple search of the available channel names is acceptable
222
223 async def get_library_artists(self) -> AsyncGenerator[Artist]:
224 """Retrieve library artists from the provider."""
225 # OPTIONAL
226 # Will only be called if you reported the LIBRARY_ARTISTS feature
227 # in the supported_features and you did not override the default sync method.
228 # It allows retrieving the library/favorite artists from your provider.
229 # Warning: Async generator:
230 # You should yield Artist objects for each artist in the library.
231 # NOTE: This is only called on each full sync of the library (at the specified interval).
232 # You are free to implement caching in your provider, as long as you return all items
233 # on each call. The Music Assistant will take care of adding/removing items from the
234 # library based on the returned items in the (default) 'sync_library' method.
235 # If you need more fine grained control over the sync process, you can override
236 # the 'sync_library' method.
237 yield Artist(
238 # A simple example of an artist object,
239 # you should replace this with actual data from your provider.
240 # Explore the Artist model for all options and descriptions.
241 item_id="123",
242 provider=self.instance_id,
243 name="Artist Name",
244 provider_mappings={
245 ProviderMapping(
246 # A provider mapping is used to provide details about this item on this provider
247 # Music Assistant differentiates between domain and instance id to account for
248 # multiple instances of the same provider.
249 # The instance_id is auto generated by MA.
250 item_id="123",
251 provider_domain=self.domain,
252 provider_instance=self.instance_id,
253 # set 'available' to false if the item is (temporary) unavailable
254 available=True,
255 audio_format=AudioFormat(
256 # provide details here about sample rate etc. if known
257 content_type=ContentType.FLAC,
258 ),
259 )
260 },
261 )
262
263 async def get_library_albums(self) -> AsyncGenerator[Album]:
264 """Retrieve library albums from the provider."""
265 # OPTIONAL
266 # Will only be called if you reported the LIBRARY_ALBUMS feature
267 # in the supported_features and you did not override the default sync method.
268 # It allows retrieving the library/favorite albums from your provider.
269 # Warning: Async generator:
270 # You should yield Album objects for each album in the library.
271 # NOTE: This is only called on each full sync of the library (at the specified interval).
272 # You are free to implement caching in your provider, as long as you return all items
273 # on each call. The Music Assistant will take care of adding/removing items from the
274 # library based on the returned items in the (default) 'sync_library' method.
275 # If you need more fine grained control over the sync process, you can override
276 # the 'sync_library' method.
277 yield # type: ignore[misc]
278
279 async def get_library_tracks(self) -> AsyncGenerator[Track]:
280 """Retrieve library tracks from the provider."""
281 # OPTIONAL
282 # Will only be called if you reported the LIBRARY_TRACKS feature
283 # in the supported_features and you did not override the default sync method.
284 # It allows retrieving the library/favorite tracks from your provider.
285 # Warning: Async generator:
286 # You should yield Track objects for each track in the library.
287 # NOTE: This is only called on each full sync of the library (at the specified interval).
288 # You are free to implement caching in your provider, as long as you return all items
289 # on each call. The Music Assistant will take care of adding/removing items from the
290 # library based on the returned items in the (default) 'sync_library' method.
291 # If you need more fine grained control over the sync process, you can override
292 # the 'sync_library' method.
293 yield # type: ignore[misc]
294
295 async def get_library_playlists(self) -> AsyncGenerator[Playlist]:
296 """Retrieve library/subscribed playlists from the provider."""
297 # OPTIONAL
298 # Will only be called if you reported the LIBRARY_PLAYLISTS feature
299 # in the supported_features and you did not override the default sync method.
300 # It allows retrieving the library/favorite playlists from your provider.
301 # Warning: Async generator:
302 # You should yield Playlist objects for each playlist in the library.
303 # NOTE: This is only called on each full sync of the library (at the specified interval).
304 # You are free to implement caching in your provider, as long as you return all items
305 # on each call. The Music Assistant will take care of adding/removing items from the
306 # library based on the returned items in the (default) 'sync_library' method.
307 # If you need more fine grained control over the sync process, you can override
308 # the 'sync_library' method.
309 yield # type: ignore[misc]
310
311 async def get_library_radios(self) -> AsyncGenerator[Radio]:
312 """Retrieve library/subscribed radio stations from the provider."""
313 # OPTIONAL
314 # Will only be called if you reported the LIBRARY_RADIOS feature
315 # in the supported_features and you did not override the default sync method.
316 # It allows retrieving the library/favorite radio stations from your provider.
317 # To be clear, this is only implemented (and the LIBRARY_RADIOS feature declared
318 # if the originating provider supports the concept of favourites or a library of
319 # its own. This method synchronises the providers library with MA's library. It
320 # is not acceptable to automatically add all channels to the users library.
321
322 # Warning: Async generator:
323 # You should yield Radio objects for each radio station in the library.
324 # NOTE: This is only called on each full sync of the library (at the specified interval).
325 # You are free to implement caching in your provider, as long as you return all items
326 # on each call. The Music Assistant will take care of adding/removing items from the
327 # library based on the returned items in the (default) 'sync_library' method.
328 # If you need more fine grained control over the sync process, you can override
329 # the 'sync_library' method.
330 yield # type: ignore[misc]
331
332 async def get_artist(self, prov_artist_id: str) -> Artist: # type: ignore[empty-body]
333 """Get full artist details by id."""
334 # Get full details of a single Artist.
335 # Mandatory only if you reported LIBRARY_ARTISTS in the supported_features.
336 # NOTE: Because this is often static data, it is advised to apply caching here
337 # to avoid too many calls to the provider's API.
338 # You can use the @use_cache decorator from music_assistant.controllers.cache
339 # to easily apply caching to this method.
340
341 async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: # type: ignore[empty-body]
342 """Get a list of all albums for the given artist."""
343 # Get a list of all albums for the given artist.
344 # Mandatory only if you reported ARTIST_ALBUMS in the supported_features.
345 # NOTE: Because this is often static data, it is advised to apply caching here
346 # to avoid too many calls to the provider's API.
347 # You can use the @use_cache decorator from music_assistant.controllers.cache
348 # to easily apply caching to this method.
349 # As this returns a collection that also serves as good fallback data, decorate it with
350 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
351 # That serves the stale result instantly while refreshing it in the background.
352
353 async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: # type: ignore[empty-body]
354 """Get a list of most popular tracks for the given artist."""
355 # Get a list of most popular tracks for the given artist.
356 # Mandatory only if you reported ARTIST_TOPTRACKS in the supported_features.
357 # Note that (local) file based providers will simply return all artist tracks here.
358 # NOTE: Because this is often static data, it is advised to apply caching here
359 # to avoid too many calls to the provider's API.
360 # You can use the @use_cache decorator from music_assistant.controllers.cache
361 # to easily apply caching to this method.
362 # As this returns a collection that also serves as good fallback data, decorate it with
363 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
364 # That serves the stale result instantly while refreshing it in the background.
365
366 async def get_album(self, prov_album_id: str) -> Album: # type: ignore[empty-body]
367 """Get full album details by id."""
368 # Get full details of a single Album.
369 # Mandatory only if you reported LIBRARY_ALBUMS in the supported_features.
370 # NOTE: Because this is often static data, it is advised to apply caching here
371 # to avoid too many calls to the provider's API.
372 # You can use the @use_cache decorator from music_assistant.controllers.cache
373 # to easily apply caching to this method.
374
375 async def get_track(self, prov_track_id: str) -> Track: # type: ignore[empty-body]
376 """Get full track details by id."""
377 # Get full details of a single Track.
378 # Mandatory only if you reported LIBRARY_TRACKS in the supported_features.
379 # NOTE: Because this is often static data, it is advised to apply caching here
380 # to avoid too many calls to the provider's API.
381 # You can use the @use_cache decorator from music_assistant.controllers.cache
382 # to easily apply caching to this method.
383
384 async def get_playlist(self, prov_playlist_id: str) -> Playlist: # type: ignore[empty-body]
385 """Get full playlist details by id."""
386 # Get full details of a single Playlist.
387 # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported
388 # NOTE: Because this is often static data, it is advised to apply caching here
389 # to avoid too many calls to the provider's API.
390 # You can use the @use_cache decorator from music_assistant.controllers.cache
391 # to easily apply caching to this method.
392
393 async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[empty-body]
394 """Get full radio details by id."""
395 # Get full details of a single Radio station.
396 # Mandatory only if you reported LIBRARY_RADIOS in the supported_features.
397 # NOTE: Because this is often static data, it is advised to apply caching here
398 # to avoid too many calls to the provider's API.
399 # You can use the @use_cache decorator from music_assistant.controllers.cache
400 # to easily apply caching to this method.
401
402 async def get_album_tracks( # type: ignore[empty-body]
403 self,
404 prov_album_id: str,
405 ) -> list[Track]:
406 """Get album tracks for given album id."""
407 # Get all tracks for a given album.
408 # Mandatory only if you reported ARTIST_ALBUMS in the supported_features.
409 # NOTE: Because this is often static data, it is advised to apply caching here
410 # to avoid too many calls to the provider's API.
411 # You can use the @use_cache decorator from music_assistant.controllers.cache
412 # to easily apply caching to this method.
413 # As this returns a collection that also serves as good fallback data, decorate it with
414 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
415 # That serves the stale result instantly while refreshing it in the background.
416
417 async def get_playlist_tracks( # type: ignore[empty-body]
418 self,
419 prov_playlist_id: str,
420 page: int = 0,
421 ) -> list[Track]:
422 """Get all playlist tracks for given playlist id."""
423 # Get all tracks for a given playlist.
424 # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported_features.
425 # NOTE: It is advised to apply caching here (if possible)
426 # to avoid too many calls to the provider's API.
427 # You can use the @use_cache decorator from music_assistant.controllers.cache
428 # to easily apply caching to this method.
429 # As this returns a collection that also serves as good fallback data, decorate it with
430 # allow_expired_cache=True, e.g. @use_cache(3600 * 3, allow_expired_cache=True).
431 # That serves the stale result instantly while refreshing it in the background.
432
433 async def library_add(self, item: MediaItemType) -> bool:
434 """Add item to provider's library. Return true on success."""
435 # Add an item to your provider's library.
436 # This is only called if the provider supports the EDIT feature for the media type.
437 return True
438
439 async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
440 """Remove item from provider's library. Return true on success."""
441 # Remove an item from your provider's library.
442 # This is only called if the provider supports the EDIT feature for the media type.
443 return True
444
445 async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None:
446 """Add track(s) to playlist."""
447 # Add track(s) to a playlist.
448 # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature.
449
450 async def remove_playlist_tracks(
451 self, prov_playlist_id: str, positions_to_remove: tuple[int, ...]
452 ) -> None:
453 """Remove track(s) from playlist."""
454 # Remove track(s) from a playlist.
455 # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature.
456
457 async def create_playlist(self, name: str, media_types: set[MediaType]) -> Playlist: # type: ignore[empty-body]
458 """Create a new playlist on provider with given name."""
459 # Create a new playlist on the provider.
460 # This is only called if the provider supports the PLAYLIST_CREATE feature.
461
462 async def get_similar_tracks( # type: ignore[empty-body]
463 self, prov_track_id: str, limit: int = 25
464 ) -> list[Track]:
465 """Retrieve a dynamic list of similar tracks based on the provided track."""
466 # Get a list of similar tracks based on the provided track.
467 # This is only called if the provider supports the SIMILAR_TRACKS feature.
468 # NOTE: It is advised to apply caching here (if possible)
469 # to avoid too many calls to the provider's API.
470 # You can use the @use_cache decorator from music_assistant.controllers.cache
471 # to easily apply caching to this method.
472 # As this returns a collection that also serves as good fallback data, decorate it with
473 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
474 # That serves the stale result instantly while refreshing it in the background.
475
476 async def get_resume_position( # type: ignore[empty-body]
477 self, item_id: str, media_type: MediaType
478 ) -> tuple[bool, int, datetime | None]:
479 """
480 Get progress (resume point) details for the given Audiobook or Podcast episode.
481
482 This is a separate call from the regular get_item call to ensure the resume position
483 is always up-to-date and because a lot providers have this info present on a dedicated
484 endpoint.
485
486 Will be called right before playback starts to ensure the resume position is correct.
487
488 Returns a boolean with the fully_played status
489 and an integer with the resume position in ms,
490 and an optional timestamp as datetime when this resume position was set.
491 """
492 # optional function to get the resume position of a audiobook or podcast episode
493 # only implement this if your provider supports providing this information!
494
495 async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
496 """Get streamdetails for a track/radio."""
497 # Get stream details for a track or radio.
498 # Implementing this method is MANDATORY to allow playback.
499 # The StreamDetails contain info how Music Assistant can play the track.
500 # item_id will always be a track or radio id. Later, when/if MA supports
501 # podcasts or audiobooks, this may as well be an episode or chapter id.
502 # You should return a StreamDetails object here with the info as accurate as possible
503 # to allow Music Assistant to process the audio using ffmpeg.
504 # IMPORTANT: Streaming providers (ie. is_streaming_provider = True) are NOT allowed
505 # to cache any audio data from the provider locally. Streaming providers must always
506 # return a valid stream url in the StreamDetails with an optional encryption key in
507 # case of encrypted streams.
508 return StreamDetails(
509 provider=self.instance_id,
510 item_id=item_id,
511 audio_format=AudioFormat(
512 # provide details here about sample rate etc. if known
513 # set content type to unknown to let ffmpeg guess the codec/container
514 content_type=ContentType.UNKNOWN,
515 ),
516 media_type=MediaType.TRACK,
517 # streamtype defines how the stream is provided
518 # for most providers this will be HTTP but you can also use CUSTOM
519 # to provide a custom stream generator in get_audio_stream.
520 stream_type=StreamType.HTTP,
521 # explore the StreamDetails model and StreamType enum for more options
522 # but the above should be the mandatory fields to set.
523 allow_seek=True,
524 # set allow_seek to True if the stream may be seeked
525 can_seek=True,
526 # set can_seek to True if the stream supports seeking
527 )
528
529 async def get_audio_stream(
530 self, streamdetails: StreamDetails, seek_position: int = 0
531 ) -> AsyncGenerator[bytes]:
532 """
533 Return the (custom) audio stream for the provider item.
534
535 Will only be called when the stream_type is set to CUSTOM.
536 """
537 # this is an async generator that should yield raw audio bytes
538 # for the given streamdetails. You can use this to provide a custom
539 # stream generator for the audio stream. This is only called when the
540 # stream_type is set to CUSTOM in the get_stream_details method.
541 yield # type: ignore[misc]
542
543 async def on_streamed(
544 self,
545 streamdetails: StreamDetails,
546 ) -> None:
547 """
548 Handle callback when given streamdetails completed streaming.
549
550 To get the number of seconds streamed, see streamdetails.seconds_streamed.
551 To get the number of seconds seeked/skipped, see streamdetails.seek_position.
552 Note that seconds_streamed is the total streamed seconds, so without seeked time.
553
554 NOTE: Due to internal and player buffering,
555 this may be called in advance of the actual completion.
556 """
557 # This is an OPTIONAL callback that is called when an item has been streamed.
558 # You can use this e.g. for playback reporting or statistics.
559
560 async def on_played(
561 self,
562 media_type: MediaType,
563 prov_item_id: str,
564 fully_played: bool,
565 position: int,
566 media_item: MediaItemType,
567 is_playing: bool = False,
568 ) -> None:
569 """
570 Handle callback when a (playable) media item has been played.
571
572 This is called by the Queue controller when;
573 - a track has been fully played
574 - a track has been stopped (or skipped) after being played
575 - every 30s when a track is playing
576
577 Fully played is True when the track has been played to the end.
578
579 Position is the last known position of the track in seconds, to sync resume state.
580 When fully_played is set to false and position is 0,
581 the user marked the item as unplayed in the UI.
582
583 is_playing is True when the track is currently playing.
584
585 media_item is the full media item details of the played/playing track.
586 """
587 # This is an OPTIONAL callback that is called when an item has been streamed.
588 # You can use this e.g. for playback reporting or statistics.
589
590 async def resolve_image(self, path: str) -> str | bytes:
591 """
592 Resolve an image from an image path.
593
594 This either returns (a generator to get) raw bytes of the image or
595 a string with an http(s) URL or local path that is accessible from the server.
596 """
597 # This is an OPTIONAL method that you can implement to resolve image paths.
598 # This is used to resolve image paths that are returned in the MediaItems.
599 # You can return a URL to an image or a generator that yields the raw bytes of the image.
600 # This will only be called when you set 'remotely_accessible'
601 # to false in a MediaItemImage object.
602 return path
603
604 async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]:
605 """
606 Browse this provider's items.
607
608 :param path: The path to browse, (e.g. provider_id://artists).
609 """
610 # Browse your provider's recommendations/media items.
611 # This is only called if you reported the BROWSE feature in the supported_features.
612 # You should return a list of MediaItems or ItemMappings for the given path.
613 # Note that you can return nested levels with BrowseFolder items.
614
615 # Ordinarily if the LIBRARY_* feature is declared then browse()
616 # is not implemented here as the MusicProvider base model has a default
617 # implementation which calls the get_library_*() methods.
618 # In this case the expectation is that adding to the library is done via search().
619 # For radio, where the LIBRARY_RADIOS feature is not declared, then browse() should
620 # be implemented
621
622 return []
623
624 async def get_recommendations(self) -> list[RecommendationFolder]:
625 """
626 Get this provider's available recommendation rows, without items.
627
628 Must be fast: return static or cached row descriptors only, without
629 live backend calls. The items for a row are fetched separately
630 through get_recommendation_items.
631 """
632 # This is only called if you reported the RECOMMENDATIONS feature
633 # in the supported_features.
634 # Return one RecommendationFolder per recommendation row, filling in only
635 # the descriptor fields and leaving 'items' at its (empty) default, e.g.:
636 # RecommendationFolder(
637 # item_id="new_releases",
638 # provider=self.instance_id,
639 # name="New Releases",
640 # translation_key="new_releases",
641 # icon="mdi-album",
642 # )
643 # Keep each row's item_id STABLE across calls and releases: the frontend
644 # stores user preferences (such as which rows are enabled) keyed on it.
645 # This method must be fast: do NOT perform any backend/network calls here.
646 # Local checks are fine, e.g. omitting rows that require a logged-in account.
647 # If your provider can only fetch its recommendations as one bulk payload,
648 # use the RecommendationPayloadMixin (music_assistant.models.recommendation_payload):
649 # implement _fetch_recommendation_payload() and serve this method from
650 # _recommendation_rows_from_payload(). List the mixin before the provider base
651 # class (class MyProvider(RecommendationPayloadMixin, MusicProvider)) so its
652 # unload() override can cancel in-flight payload tasks.
653 return []
654
655 async def get_recommendation_items(
656 self, item_id: str
657 ) -> UniqueList[MediaItemType | ItemMapping | BrowseFolder]:
658 """
659 Get the items for a single recommendation row.
660
661 :param item_id: The item_id of the row, as returned by get_recommendations.
662 """
663 # This is only called if you reported the RECOMMENDATIONS feature
664 # in the supported_features.
665 # Live backend fetches belong here: match on the given item_id and
666 # fetch/build the items for just that row, e.g.:
667 # if item_id == "new_releases":
668 # return UniqueList(await self._fetch_new_releases())
669 # An unknown item_id must return an empty UniqueList (do not raise).
670 # NOTE: It is advised to apply caching here (if possible) to avoid too
671 # many calls to the provider's API. You can use the @use_cache decorator
672 # from music_assistant.controllers.cache: it keys on the item_id argument,
673 # giving each row its own cache entry.
674 # If you use the RecommendationPayloadMixin (see get_recommendations),
675 # serve this method from _recommendation_items_from_payload(item_id) instead.
676 return UniqueList()
677
678 async def sync_library(self, media_type: MediaType) -> None:
679 """Run library sync for this provider."""
680 # Run a full sync of the library for the given media type.
681 # This is called by the music controller to sync items from your provider to the MA library.
682 # As a generic rule of thumb the default implementation within the MusicProvider
683 # base model should be sufficient for most (streaming) providers.
684 # If you need to do some custom sync logic, you can override this method.
685 # For example the filesystem provider in MA, overrides this method to scan the filesystem.
686