/
/
/
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 @property
204 def supported_media_types(self) -> set[MediaType]:
205 """
206 Return the media types this provider can serve.
207
208 Defaults to the media types the provider declares library support for.
209 Override for providers that can serve (search/stream) media types they
210 cannot list as library items, so they are eligible for search-based
211 lookups such as cross-provider matching and versions.
212 """
213 # OPTIONAL - the default (derived from the LIBRARY_* features) is usually correct.
214 return super().supported_media_types
215
216 async def search( # type: ignore[empty-body]
217 self,
218 search_query: str,
219 media_types: list[MediaType],
220 limit: int = 5,
221 ) -> SearchResults:
222 """
223 Perform search on musicprovider.
224
225 :param search_query: Search query.
226 :param media_types: A list of media_types to include.
227 :param limit: Number of items to return in the search (per type).
228 """
229 # OPTIONAL
230 # Will only be called if you reported the SEARCH feature in the supported_features.
231 # It allows searching your provider for media items.
232 # See the model for SearchResults for more information on what to return, but
233 # in general you should return a list of MediaItems for each media type.
234 # For radio, a simple search of the available channel names is acceptable
235
236 async def get_library_artists(self) -> AsyncGenerator[Artist]:
237 """Retrieve library artists from the provider."""
238 # OPTIONAL
239 # Will only be called if you reported the LIBRARY_ARTISTS feature
240 # in the supported_features and you did not override the default sync method.
241 # It allows retrieving the library/favorite artists from your provider.
242 # Warning: Async generator:
243 # You should yield Artist objects for each artist in the library.
244 # NOTE: This is only called on each full sync of the library (at the specified interval).
245 # You are free to implement caching in your provider, as long as you return all items
246 # on each call. The Music Assistant will take care of adding/removing items from the
247 # library based on the returned items in the (default) 'sync_library' method.
248 # If you need more fine grained control over the sync process, you can override
249 # the 'sync_library' method.
250 yield Artist(
251 # A simple example of an artist object,
252 # you should replace this with actual data from your provider.
253 # Explore the Artist model for all options and descriptions.
254 item_id="123",
255 provider=self.instance_id,
256 name="Artist Name",
257 provider_mappings={
258 ProviderMapping(
259 # A provider mapping is used to provide details about this item on this provider
260 # Music Assistant differentiates between domain and instance id to account for
261 # multiple instances of the same provider.
262 # The instance_id is auto generated by MA.
263 item_id="123",
264 provider_domain=self.domain,
265 provider_instance=self.instance_id,
266 # set 'available' to false if the item is (temporary) unavailable
267 available=True,
268 audio_format=AudioFormat(
269 # provide details here about sample rate etc. if known
270 content_type=ContentType.FLAC,
271 ),
272 )
273 },
274 )
275
276 async def get_library_albums(self) -> AsyncGenerator[Album]:
277 """Retrieve library albums from the provider."""
278 # OPTIONAL
279 # Will only be called if you reported the LIBRARY_ALBUMS feature
280 # in the supported_features and you did not override the default sync method.
281 # It allows retrieving the library/favorite albums from your provider.
282 # Warning: Async generator:
283 # You should yield Album objects for each album in the library.
284 # NOTE: This is only called on each full sync of the library (at the specified interval).
285 # You are free to implement caching in your provider, as long as you return all items
286 # on each call. The Music Assistant will take care of adding/removing items from the
287 # library based on the returned items in the (default) 'sync_library' method.
288 # If you need more fine grained control over the sync process, you can override
289 # the 'sync_library' method.
290 yield # type: ignore[misc]
291
292 async def get_library_tracks(self) -> AsyncGenerator[Track]:
293 """Retrieve library tracks from the provider."""
294 # OPTIONAL
295 # Will only be called if you reported the LIBRARY_TRACKS feature
296 # in the supported_features and you did not override the default sync method.
297 # It allows retrieving the library/favorite tracks from your provider.
298 # Warning: Async generator:
299 # You should yield Track objects for each track in the library.
300 # NOTE: This is only called on each full sync of the library (at the specified interval).
301 # You are free to implement caching in your provider, as long as you return all items
302 # on each call. The Music Assistant will take care of adding/removing items from the
303 # library based on the returned items in the (default) 'sync_library' method.
304 # If you need more fine grained control over the sync process, you can override
305 # the 'sync_library' method.
306 yield # type: ignore[misc]
307
308 async def get_library_playlists(self) -> AsyncGenerator[Playlist]:
309 """Retrieve library/subscribed playlists from the provider."""
310 # OPTIONAL
311 # Will only be called if you reported the LIBRARY_PLAYLISTS feature
312 # in the supported_features and you did not override the default sync method.
313 # It allows retrieving the library/favorite playlists from your provider.
314 # Warning: Async generator:
315 # You should yield Playlist objects for each playlist in the library.
316 # NOTE: This is only called on each full sync of the library (at the specified interval).
317 # You are free to implement caching in your provider, as long as you return all items
318 # on each call. The Music Assistant will take care of adding/removing items from the
319 # library based on the returned items in the (default) 'sync_library' method.
320 # If you need more fine grained control over the sync process, you can override
321 # the 'sync_library' method.
322 yield # type: ignore[misc]
323
324 async def get_library_radios(self) -> AsyncGenerator[Radio]:
325 """Retrieve library/subscribed radio stations from the provider."""
326 # OPTIONAL
327 # Will only be called if you reported the LIBRARY_RADIOS feature
328 # in the supported_features and you did not override the default sync method.
329 # It allows retrieving the library/favorite radio stations from your provider.
330 # To be clear, this is only implemented (and the LIBRARY_RADIOS feature declared
331 # if the originating provider supports the concept of favourites or a library of
332 # its own. This method synchronises the providers library with MA's library. It
333 # is not acceptable to automatically add all channels to the users library.
334
335 # Warning: Async generator:
336 # You should yield Radio objects for each radio station in the library.
337 # NOTE: This is only called on each full sync of the library (at the specified interval).
338 # You are free to implement caching in your provider, as long as you return all items
339 # on each call. The Music Assistant will take care of adding/removing items from the
340 # library based on the returned items in the (default) 'sync_library' method.
341 # If you need more fine grained control over the sync process, you can override
342 # the 'sync_library' method.
343 yield # type: ignore[misc]
344
345 async def get_artist(self, prov_artist_id: str) -> Artist: # type: ignore[empty-body]
346 """Get full artist details by id."""
347 # Get full details of a single Artist.
348 # Mandatory only if you reported LIBRARY_ARTISTS in the supported_features.
349 # NOTE: Because this is often static data, it is advised to apply caching here
350 # to avoid too many calls to the provider's API.
351 # You can use the @use_cache decorator from music_assistant.controllers.cache
352 # to easily apply caching to this method.
353
354 async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: # type: ignore[empty-body]
355 """Get a list of all albums for the given artist."""
356 # Get a list of all albums for the given artist.
357 # Mandatory only if you reported ARTIST_ALBUMS in the supported_features.
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_artist_toptracks(self, prov_artist_id: str) -> list[Track]: # type: ignore[empty-body]
367 """Get a list of most popular tracks for the given artist."""
368 # Get a list of most popular tracks for the given artist.
369 # Mandatory only if you reported ARTIST_TOPTRACKS in the supported_features.
370 # Note that (local) file based providers will simply return all artist tracks here.
371 # NOTE: Because this is often static data, it is advised to apply caching here
372 # to avoid too many calls to the provider's API.
373 # You can use the @use_cache decorator from music_assistant.controllers.cache
374 # to easily apply caching to this method.
375 # As this returns a collection that also serves as good fallback data, decorate it with
376 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
377 # That serves the stale result instantly while refreshing it in the background.
378
379 async def get_album(self, prov_album_id: str) -> Album: # type: ignore[empty-body]
380 """Get full album details by id."""
381 # Get full details of a single Album.
382 # Mandatory only if you reported LIBRARY_ALBUMS in the supported_features.
383 # NOTE: Because this is often static data, it is advised to apply caching here
384 # to avoid too many calls to the provider's API.
385 # You can use the @use_cache decorator from music_assistant.controllers.cache
386 # to easily apply caching to this method.
387
388 async def get_track(self, prov_track_id: str) -> Track: # type: ignore[empty-body]
389 """Get full track details by id."""
390 # Get full details of a single Track.
391 # Mandatory only if you reported LIBRARY_TRACKS in the supported_features.
392 # NOTE: Because this is often static data, it is advised to apply caching here
393 # to avoid too many calls to the provider's API.
394 # You can use the @use_cache decorator from music_assistant.controllers.cache
395 # to easily apply caching to this method.
396
397 async def get_playlist(self, prov_playlist_id: str) -> Playlist: # type: ignore[empty-body]
398 """Get full playlist details by id."""
399 # Get full details of a single Playlist.
400 # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported
401 # NOTE: Because this is often static data, it is advised to apply caching here
402 # to avoid too many calls to the provider's API.
403 # You can use the @use_cache decorator from music_assistant.controllers.cache
404 # to easily apply caching to this method.
405
406 async def get_radio(self, prov_radio_id: str) -> Radio: # type: ignore[empty-body]
407 """Get full radio details by id."""
408 # Get full details of a single Radio station.
409 # Mandatory only if you reported LIBRARY_RADIOS in the supported_features.
410 # NOTE: Because this is often static data, it is advised to apply caching here
411 # to avoid too many calls to the provider's API.
412 # You can use the @use_cache decorator from music_assistant.controllers.cache
413 # to easily apply caching to this method.
414
415 async def get_album_tracks( # type: ignore[empty-body]
416 self,
417 prov_album_id: str,
418 ) -> list[Track]:
419 """Get album tracks for given album id."""
420 # Get all tracks for a given album.
421 # Mandatory only if you reported ARTIST_ALBUMS in the supported_features.
422 # NOTE: Because this is often static data, it is advised to apply caching here
423 # to avoid too many calls to the provider's API.
424 # You can use the @use_cache decorator from music_assistant.controllers.cache
425 # to easily apply caching to this method.
426 # As this returns a collection that also serves as good fallback data, decorate it with
427 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
428 # That serves the stale result instantly while refreshing it in the background.
429
430 async def get_playlist_tracks( # type: ignore[empty-body]
431 self,
432 prov_playlist_id: str,
433 page: int = 0,
434 ) -> list[Track]:
435 """Get all playlist tracks for given playlist id."""
436 # Get all tracks for a given playlist.
437 # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported_features.
438 # NOTE: It is advised to apply caching here (if possible)
439 # to avoid too many calls to the provider's API.
440 # You can use the @use_cache decorator from music_assistant.controllers.cache
441 # to easily apply caching to this method.
442 # As this returns a collection that also serves as good fallback data, decorate it with
443 # allow_expired_cache=True, e.g. @use_cache(3600 * 3, allow_expired_cache=True).
444 # That serves the stale result instantly while refreshing it in the background.
445
446 async def library_add(self, item: MediaItemType) -> bool:
447 """Add item to provider's library. Return true on success."""
448 # Add an item to your provider's library.
449 # This is only called if the provider supports the EDIT feature for the media type.
450 return True
451
452 async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
453 """Remove item from provider's library. Return true on success."""
454 # Remove an item from your provider's library.
455 # This is only called if the provider supports the EDIT feature for the media type.
456 return True
457
458 async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None:
459 """Add track(s) to playlist."""
460 # Add track(s) to a playlist.
461 # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature.
462
463 async def remove_playlist_tracks(
464 self, prov_playlist_id: str, positions_to_remove: tuple[int, ...]
465 ) -> None:
466 """Remove track(s) from playlist."""
467 # Remove track(s) from a playlist.
468 # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature.
469
470 async def create_playlist(self, name: str, media_types: set[MediaType]) -> Playlist: # type: ignore[empty-body]
471 """Create a new playlist on provider with given name."""
472 # Create a new playlist on the provider.
473 # This is only called if the provider supports the PLAYLIST_CREATE feature.
474
475 async def get_similar_tracks( # type: ignore[empty-body]
476 self, prov_track_id: str, limit: int = 25
477 ) -> list[Track]:
478 """Retrieve a dynamic list of similar tracks based on the provided track."""
479 # Get a list of similar tracks based on the provided track.
480 # This is only called if the provider supports the SIMILAR_TRACKS feature.
481 # NOTE: It is advised to apply caching here (if possible)
482 # to avoid too many calls to the provider's API.
483 # You can use the @use_cache decorator from music_assistant.controllers.cache
484 # to easily apply caching to this method.
485 # As this returns a collection that also serves as good fallback data, decorate it with
486 # allow_expired_cache=True, e.g. @use_cache(3600 * 24, allow_expired_cache=True).
487 # That serves the stale result instantly while refreshing it in the background.
488
489 async def get_resume_position( # type: ignore[empty-body]
490 self, item_id: str, media_type: MediaType
491 ) -> tuple[bool, int, datetime | None]:
492 """
493 Get progress (resume point) details for the given Audiobook or Podcast episode.
494
495 This is a separate call from the regular get_item call to ensure the resume position
496 is always up-to-date and because a lot providers have this info present on a dedicated
497 endpoint.
498
499 Will be called right before playback starts to ensure the resume position is correct.
500
501 Returns a boolean with the fully_played status
502 and an integer with the resume position in ms,
503 and an optional timestamp as datetime when this resume position was set.
504 """
505 # optional function to get the resume position of a audiobook or podcast episode
506 # only implement this if your provider supports providing this information!
507
508 async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
509 """Get streamdetails for a track/radio."""
510 # Get stream details for a track or radio.
511 # Implementing this method is MANDATORY to allow playback.
512 # The StreamDetails contain info how Music Assistant can play the track.
513 # item_id will always be a track or radio id. Later, when/if MA supports
514 # podcasts or audiobooks, this may as well be an episode or chapter id.
515 # You should return a StreamDetails object here with the info as accurate as possible
516 # to allow Music Assistant to process the audio using ffmpeg.
517 # IMPORTANT: Streaming providers (ie. is_streaming_provider = True) are NOT allowed
518 # to cache any audio data from the provider locally. Streaming providers must always
519 # return a valid stream url in the StreamDetails with an optional encryption key in
520 # case of encrypted streams.
521 return StreamDetails(
522 provider=self.instance_id,
523 item_id=item_id,
524 audio_format=AudioFormat(
525 # provide details here about sample rate etc. if known
526 # set content type to unknown to let ffmpeg guess the codec/container
527 content_type=ContentType.UNKNOWN,
528 ),
529 media_type=MediaType.TRACK,
530 # streamtype defines how the stream is provided
531 # for most providers this will be HTTP but you can also use CUSTOM
532 # to provide a custom stream generator in get_audio_stream.
533 stream_type=StreamType.HTTP,
534 # explore the StreamDetails model and StreamType enum for more options
535 # but the above should be the mandatory fields to set.
536 allow_seek=True,
537 # set allow_seek to True if the stream may be seeked
538 can_seek=True,
539 # set can_seek to True if the stream supports seeking
540 )
541
542 async def get_audio_stream(
543 self, streamdetails: StreamDetails, seek_position: int = 0
544 ) -> AsyncGenerator[bytes]:
545 """
546 Return the (custom) audio stream for the provider item.
547
548 Will only be called when the stream_type is set to CUSTOM.
549 """
550 # this is an async generator that should yield raw audio bytes
551 # for the given streamdetails. You can use this to provide a custom
552 # stream generator for the audio stream. This is only called when the
553 # stream_type is set to CUSTOM in the get_stream_details method.
554 yield # type: ignore[misc]
555
556 async def on_streamed(
557 self,
558 streamdetails: StreamDetails,
559 ) -> None:
560 """
561 Handle callback when given streamdetails completed streaming.
562
563 To get the number of seconds streamed, see streamdetails.seconds_streamed.
564 To get the number of seconds seeked/skipped, see streamdetails.seek_position.
565 Note that seconds_streamed is the total streamed seconds, so without seeked time.
566
567 NOTE: Due to internal and player buffering,
568 this may be called in advance of the actual completion.
569 """
570 # This is an OPTIONAL callback that is called when an item has been streamed.
571 # You can use this e.g. for playback reporting or statistics.
572
573 async def on_played(
574 self,
575 media_type: MediaType,
576 prov_item_id: str,
577 fully_played: bool,
578 position: int,
579 media_item: MediaItemType,
580 is_playing: bool = False,
581 ) -> None:
582 """
583 Handle callback when a (playable) media item has been played.
584
585 This is called by the Queue controller when;
586 - a track has been fully played
587 - a track has been stopped (or skipped) after being played
588 - every 30s when a track is playing
589
590 Fully played is True when the track has been played to the end.
591
592 Position is the last known position of the track in seconds, to sync resume state.
593 When fully_played is set to false and position is 0,
594 the user marked the item as unplayed in the UI.
595
596 is_playing is True when the track is currently playing.
597
598 media_item is the full media item details of the played/playing track.
599 """
600 # This is an OPTIONAL callback that is called when an item has been streamed.
601 # You can use this e.g. for playback reporting or statistics.
602
603 async def resolve_image(self, path: str) -> str | bytes:
604 """
605 Resolve an image from an image path.
606
607 This either returns (a generator to get) raw bytes of the image or
608 a string with an http(s) URL or local path that is accessible from the server.
609 """
610 # This is an OPTIONAL method that you can implement to resolve image paths.
611 # This is used to resolve image paths that are returned in the MediaItems.
612 # You can return a URL to an image or a generator that yields the raw bytes of the image.
613 # This will only be called when you set 'remotely_accessible'
614 # to false in a MediaItemImage object.
615 return path
616
617 async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]:
618 """
619 Browse this provider's items.
620
621 :param path: The path to browse, (e.g. provider_id://artists).
622 """
623 # Browse your provider's recommendations/media items.
624 # This is only called if you reported the BROWSE feature in the supported_features.
625 # You should return a list of MediaItems or ItemMappings for the given path.
626 # Note that you can return nested levels with BrowseFolder items.
627
628 # Ordinarily if the LIBRARY_* feature is declared then browse()
629 # is not implemented here as the MusicProvider base model has a default
630 # implementation which calls the get_library_*() methods.
631 # In this case the expectation is that adding to the library is done via search().
632 # For radio, where the LIBRARY_RADIOS feature is not declared, then browse() should
633 # be implemented
634
635 return []
636
637 async def get_recommendations(self) -> list[RecommendationFolder]:
638 """
639 Get this provider's available recommendation rows, without items.
640
641 Must be fast: return static or cached row descriptors only, without
642 live backend calls. The items for a row are fetched separately
643 through get_recommendation_items.
644 """
645 # This is only called if you reported the RECOMMENDATIONS feature
646 # in the supported_features.
647 # Return one RecommendationFolder per recommendation row, filling in only
648 # the descriptor fields and leaving 'items' at its (empty) default, e.g.:
649 # RecommendationFolder(
650 # item_id="new_releases",
651 # provider=self.instance_id,
652 # name="New Releases",
653 # translation_key="new_releases",
654 # icon="mdi-album",
655 # )
656 # Keep each row's item_id STABLE across calls and releases: the frontend
657 # stores user preferences (such as which rows are enabled) keyed on it.
658 # This method must be fast: do NOT perform any backend/network calls here.
659 # Local checks are fine, e.g. omitting rows that require a logged-in account.
660 # If your provider can only fetch its recommendations as one bulk payload,
661 # use the RecommendationPayloadMixin (music_assistant.models.recommendation_payload):
662 # implement _fetch_recommendation_payload() and serve this method from
663 # _recommendation_rows_from_payload(). List the mixin before the provider base
664 # class (class MyProvider(RecommendationPayloadMixin, MusicProvider)) so its
665 # unload() override can cancel in-flight payload tasks.
666 return []
667
668 async def get_recommendation_items(
669 self, item_id: str
670 ) -> UniqueList[MediaItemType | ItemMapping | BrowseFolder]:
671 """
672 Get the items for a single recommendation row.
673
674 :param item_id: The item_id of the row, as returned by get_recommendations.
675 """
676 # This is only called if you reported the RECOMMENDATIONS feature
677 # in the supported_features.
678 # Live backend fetches belong here: match on the given item_id and
679 # fetch/build the items for just that row, e.g.:
680 # if item_id == "new_releases":
681 # return UniqueList(await self._fetch_new_releases())
682 # An unknown item_id must return an empty UniqueList (do not raise).
683 # NOTE: It is advised to apply caching here (if possible) to avoid too
684 # many calls to the provider's API. You can use the @use_cache decorator
685 # from music_assistant.controllers.cache: it keys on the item_id argument,
686 # giving each row its own cache entry.
687 # If you use the RecommendationPayloadMixin (see get_recommendations),
688 # serve this method from _recommendation_items_from_payload(item_id) instead.
689 return UniqueList()
690
691 async def sync_library(self, media_type: MediaType) -> None:
692 """Run library sync for this provider."""
693 # Run a full sync of the library for the given media type.
694 # This is called by the music controller to sync items from your provider to the MA library.
695 # As a generic rule of thumb the default implementation within the MusicProvider
696 # base model should be sufficient for most (streaming) providers.
697 # If you need to do some custom sync logic, you can override this method.
698 # For example the filesystem provider in MA, overrides this method to scan the filesystem.
699