music-assistant-server

29.5 KBPY
__init__.py
29.5 KB595 lines • python
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 can need to use the several
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
27Optional is an icon.svg file that will be used as the icon for the provider in the UI,
28but we also support that you specify a material design icon in the manifest.json file.
29
30IMPORTANT NOTE:
31We strongly recommend developing on either macOS or Linux and start your development
32environment by running the setup.sh script in the scripts folder of the repository.
33This will create a virtual environment and install all dependencies needed for development.
34See also our general DEVELOPMENT.md guide in the repository for more information.
35
36"""
37
38from __future__ import annotations
39
40from collections.abc import AsyncGenerator, Sequence
41from typing import TYPE_CHECKING
42
43from music_assistant_models.enums import ContentType, MediaType, ProviderFeature, StreamType
44from music_assistant_models.media_items import (
45    Album,
46    Artist,
47    AudioFormat,
48    BrowseFolder,
49    ItemMapping,
50    MediaItemType,
51    Playlist,
52    ProviderMapping,
53    Radio,
54    RecommendationFolder,
55    SearchResults,
56    Track,
57)
58from music_assistant_models.streamdetails import StreamDetails
59
60from music_assistant.models.music_provider import MusicProvider
61
62if TYPE_CHECKING:
63    from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig
64    from music_assistant_models.provider import ProviderManifest
65
66    from music_assistant.mass import MusicAssistant
67    from music_assistant.models import ProviderInstanceType
68
69
70SUPPORTED_FEATURES = {
71    ProviderFeature.BROWSE,
72    ProviderFeature.SEARCH,
73    ProviderFeature.RECOMMENDATIONS,
74    ProviderFeature.LIBRARY_ARTISTS,
75    ProviderFeature.LIBRARY_ALBUMS,
76    ProviderFeature.LIBRARY_TRACKS,
77    ProviderFeature.LIBRARY_PLAYLISTS,
78    ProviderFeature.ARTIST_ALBUMS,
79    ProviderFeature.ARTIST_TOPTRACKS,
80    ProviderFeature.LIBRARY_ARTISTS_EDIT,
81    ProviderFeature.LIBRARY_ALBUMS_EDIT,
82    ProviderFeature.LIBRARY_TRACKS_EDIT,
83    ProviderFeature.LIBRARY_PLAYLISTS_EDIT,
84    ProviderFeature.SIMILAR_TRACKS,
85    # MANDATORY
86    # this constant should contain a set of provider-level features
87    # that your music provider supports or an empty set if none.
88    # for example 'ProviderFeature.BROWSE' if you can browse the provider's items.
89    # see the ProviderFeature enum for all available features
90}
91
92
93async def setup(
94    mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
95) -> ProviderInstanceType:
96    """Initialize provider(instance) with given configuration."""
97    # setup is called when the user wants to setup a new provider instance.
98    # you are free to do any preflight checks here and but you must return
99    #  an instance of the provider.
100    return MyDemoMusicprovider(mass, manifest, config, SUPPORTED_FEATURES)
101
102
103async def get_config_entries(
104    mass: MusicAssistant,
105    instance_id: str | None = None,
106    action: str | None = None,
107    values: dict[str, ConfigValueType] | None = None,
108) -> tuple[ConfigEntry, ...]:
109    """
110    Return Config entries to setup this provider.
111
112    instance_id: id of an existing provider instance (None if new instance setup).
113    action: [optional] action key called from config entries UI.
114    values: the (intermediate) raw values for config entries sent with the action.
115    """
116    # ruff: noqa: ARG001
117    # Config Entries are used to configure the Music Provider if needed.
118    # See the models of ConfigEntry and ConfigValueType for more information what is supported.
119    # The ConfigEntry is a dataclass that represents a single configuration entry.
120    # The ConfigValueType is an Enum that represents the type of value that
121    # can be stored in a ConfigEntry.
122    # If your provider does not need any configuration, you can return an empty tuple.
123
124    # We support flow-like configuration where you can have multiple steps of configuration
125    # using the 'action' parameter to distinguish between the different steps.
126    # The 'values' parameter contains the raw values of the config entries that were filled in
127    # by the user in the UI. This is a dictionary with the key being the config entry id
128    # and the value being the actual value filled in by the user.
129
130    # For authentication flows where the user needs to be redirected to a login page
131    # or some other external service, we have a simple helper that can help you with those steps
132    # and a callback url that you can use to redirect the user back to the Music Assistant UI.
133    # See for example the Deezer provider for an example of how to use this.
134    return ()
135
136
137class MyDemoMusicprovider(MusicProvider):
138    """
139    Example/demo Music provider.
140
141    Note that this is always subclassed from MusicProvider,
142    which in turn is a subclass of the generic Provider model.
143
144    The base implementation already takes care of some convenience methods,
145    such as the mass object and the logger. Take a look at the base class
146    for more information on what is available.
147
148    Just like with any other subclass, make sure that if you override
149    any of the default methods (such as __init__), you call the super() method.
150    In most cases its not needed to override any of the builtin methods and you only
151    implement the abc methods with your actual implementation.
152    """
153
154    async def loaded_in_mass(self) -> None:
155        """Call after the provider has been loaded."""
156        # OPTIONAL
157        # this is an optional method that you can implement if
158        # relevant or leave out completely if not needed.
159        # In most cases this can be omitted for music providers.
160
161    async def unload(self, is_removed: bool = False) -> None:
162        """
163        Handle unload/close of the provider.
164
165        Called when provider is deregistered (e.g. MA exiting or config reloading).
166        is_removed will be set to True when the provider is removed from the configuration.
167        """
168        # OPTIONAL
169        # This is an optional method that you can implement if
170        # relevant or leave out completely if not needed.
171        # It will be called when the provider is unloaded from Music Assistant.
172        # for example to disconnect from a service or clean up resources.
173
174    @property
175    def is_streaming_provider(self) -> bool:
176        """
177        Return True if the provider is a streaming provider.
178
179        This literally means that the catalog is not the same as the library contents.
180        For local based providers (files, plex), the catalog is the same as the library content.
181        It also means that data is if this provider is NOT a streaming provider,
182        data cross instances is unique, the catalog and library differs per instance.
183
184        Setting this to True will only query one instance of the provider for search and lookups.
185        Setting this to False will query all instances of this provider for search and lookups.
186        """
187        # For streaming providers return True here but for local file based providers return False.
188        return True
189
190    async def search(  # type: ignore[empty-body]
191        self,
192        search_query: str,
193        media_types: list[MediaType],
194        limit: int = 5,
195    ) -> SearchResults:
196        """Perform search on musicprovider.
197
198        :param search_query: Search query.
199        :param media_types: A list of media_types to include.
200        :param limit: Number of items to return in the search (per type).
201        """
202        # OPTIONAL
203        # Will only be called if you reported the SEARCH feature in the supported_features.
204        # It allows searching your provider for media items.
205        # See the model for SearchResults for more information on what to return, but
206        # in general you should return a list of MediaItems for each media type.
207
208    async def get_library_artists(self) -> AsyncGenerator[Artist, None]:
209        """Retrieve library artists from the provider."""
210        # OPTIONAL
211        # Will only be called if you reported the LIBRARY_ARTISTS feature
212        # in the supported_features and you did not override the default sync method.
213        # It allows retrieving the library/favorite artists from your provider.
214        # Warning: Async generator:
215        # You should yield Artist objects for each artist in the library.
216        # NOTE: This is only called on each full sync of the library (at the specified interval).
217        # You are free to implement caching in your provider, as long as you return all items
218        # on each call. The Music Assistant will take care of adding/removing items from the
219        # library based on the returned items in the (default) 'sync_library' method.
220        # If you need more fine grained control over the sync process, you can override
221        # the 'sync_library' method.
222        yield Artist(
223            # A simple example of an artist object,
224            # you should replace this with actual data from your provider.
225            # Explore the Artist model for all options and descriptions.
226            item_id="123",
227            provider=self.instance_id,
228            name="Artist Name",
229            provider_mappings={
230                ProviderMapping(
231                    # A provider mapping is used to provide details about this item on this provider
232                    # Music Assistant differentiates between domain and instance id to account for
233                    # multiple instances of the same provider.
234                    # The instance_id is auto generated by MA.
235                    item_id="123",
236                    provider_domain=self.domain,
237                    provider_instance=self.instance_id,
238                    # set 'available' to false if the item is (temporary) unavailable
239                    available=True,
240                    audio_format=AudioFormat(
241                        # provide details here about sample rate etc. if known
242                        content_type=ContentType.FLAC,
243                    ),
244                )
245            },
246        )
247
248    async def get_library_albums(self) -> AsyncGenerator[Album, None]:
249        """Retrieve library albums from the provider."""
250        # OPTIONAL
251        # Will only be called if you reported the LIBRARY_ALBUMS feature
252        # in the supported_features and you did not override the default sync method.
253        # It allows retrieving the library/favorite albums from your provider.
254        # Warning: Async generator:
255        # You should yield Album objects for each album in the library.
256        # NOTE: This is only called on each full sync of the library (at the specified interval).
257        # You are free to implement caching in your provider, as long as you return all items
258        # on each call. The Music Assistant will take care of adding/removing items from the
259        # library based on the returned items in the (default) 'sync_library' method.
260        # If you need more fine grained control over the sync process, you can override
261        # the 'sync_library' method.
262        yield  # type: ignore[misc]
263
264    async def get_library_tracks(self) -> AsyncGenerator[Track, None]:
265        """Retrieve library tracks from the provider."""
266        # OPTIONAL
267        # Will only be called if you reported the LIBRARY_TRACKS feature
268        # in the supported_features and you did not override the default sync method.
269        # It allows retrieving the library/favorite tracks from your provider.
270        # Warning: Async generator:
271        # You should yield Track objects for each track in the library.
272        # NOTE: This is only called on each full sync of the library (at the specified interval).
273        # You are free to implement caching in your provider, as long as you return all items
274        # on each call. The Music Assistant will take care of adding/removing items from the
275        # library based on the returned items in the (default) 'sync_library' method.
276        # If you need more fine grained control over the sync process, you can override
277        # the 'sync_library' method.
278        yield  # type: ignore[misc]
279
280    async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]:
281        """Retrieve library/subscribed playlists from the provider."""
282        # OPTIONAL
283        # Will only be called if you reported the LIBRARY_PLAYLISTS feature
284        # in the supported_features and you did not override the default sync method.
285        # It allows retrieving the library/favorite playlists from your provider.
286        # Warning: Async generator:
287        # You should yield Playlist objects for each playlist in the library.
288        # NOTE: This is only called on each full sync of the library (at the specified interval).
289        # You are free to implement caching in your provider, as long as you return all items
290        # on each call. The Music Assistant will take care of adding/removing items from the
291        # library based on the returned items in the (default) 'sync_library' method.
292        # If you need more fine grained control over the sync process, you can override
293        # the 'sync_library' method.
294        yield  # type: ignore[misc]
295
296    async def get_library_radios(self) -> AsyncGenerator[Radio, None]:
297        """Retrieve library/subscribed radio stations from the provider."""
298        # OPTIONAL
299        # Will only be called if you reported the LIBRARY_RADIOS feature
300        # in the supported_features and you did not override the default sync method.
301        # It allows retrieving the library/favorite radio stations from your provider.
302        # Warning: Async generator:
303        # You should yield Radio objects for each radio station in the library.
304        # NOTE: This is only called on each full sync of the library (at the specified interval).
305        # You are free to implement caching in your provider, as long as you return all items
306        # on each call. The Music Assistant will take care of adding/removing items from the
307        # library based on the returned items in the (default) 'sync_library' method.
308        # If you need more fine grained control over the sync process, you can override
309        # the 'sync_library' method.
310        yield  # type: ignore[misc]
311
312    async def get_artist(self, prov_artist_id: str) -> Artist:  # type: ignore[empty-body]
313        """Get full artist details by id."""
314        # Get full details of a single Artist.
315        # Mandatory only if you reported LIBRARY_ARTISTS in the supported_features.
316        # NOTE: Because this is often static data, it is advised to apply caching here
317        # to avoid too many calls to the provider's API.
318        # You can use the @use_cache decorator from music_assistant.controllers.cache
319        # to easily apply caching to this method.
320
321    async def get_artist_albums(self, prov_artist_id: str) -> list[Album]:  # type: ignore[empty-body]
322        """Get a list of all albums for the given artist."""
323        # Get a list of all albums for the given artist.
324        # Mandatory only if you reported ARTIST_ALBUMS in the supported_features.
325        # NOTE: Because this is often static data, it is advised to apply caching here
326        # to avoid too many calls to the provider's API.
327        # You can use the @use_cache decorator from music_assistant.controllers.cache
328        # to easily apply caching to this method.
329
330    async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]:  # type: ignore[empty-body]
331        """Get a list of most popular tracks for the given artist."""
332        # Get a list of most popular tracks for the given artist.
333        # Mandatory only if you reported ARTIST_TOPTRACKS in the supported_features.
334        # Note that (local) file based providers will simply return all artist tracks here.
335        # NOTE: Because this is often static data, it is advised to apply caching here
336        # to avoid too many calls to the provider's API.
337        # You can use the @use_cache decorator from music_assistant.controllers.cache
338        # to easily apply caching to this method.
339
340    async def get_album(self, prov_album_id: str) -> Album:  # type: ignore[empty-body]
341        """Get full album details by id."""
342        # Get full details of a single Album.
343        # Mandatory only if you reported LIBRARY_ALBUMS in the supported_features.
344        # NOTE: Because this is often static data, it is advised to apply caching here
345        # to avoid too many calls to the provider's API.
346        # You can use the @use_cache decorator from music_assistant.controllers.cache
347        # to easily apply caching to this method.
348
349    async def get_track(self, prov_track_id: str) -> Track:  # type: ignore[empty-body]
350        """Get full track details by id."""
351        # Get full details of a single Track.
352        # Mandatory only if you reported LIBRARY_TRACKS in the supported_features.
353        # NOTE: Because this is often static data, it is advised to apply caching here
354        # to avoid too many calls to the provider's API.
355        # You can use the @use_cache decorator from music_assistant.controllers.cache
356        # to easily apply caching to this method.
357
358    async def get_playlist(self, prov_playlist_id: str) -> Playlist:  # type: ignore[empty-body]
359        """Get full playlist details by id."""
360        # Get full details of a single Playlist.
361        # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported
362        # NOTE: Because this is often static data, it is advised to apply caching here
363        # to avoid too many calls to the provider's API.
364        # You can use the @use_cache decorator from music_assistant.controllers.cache
365        # to easily apply caching to this method.
366
367    async def get_radio(self, prov_radio_id: str) -> Radio:  # type: ignore[empty-body]
368        """Get full radio details by id."""
369        # Get full details of a single Radio station.
370        # Mandatory only if you reported LIBRARY_RADIOS in the supported_features.
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
376    async def get_album_tracks(  # type: ignore[empty-body]
377        self,
378        prov_album_id: str,
379    ) -> list[Track]:
380        """Get album tracks for given album id."""
381        # Get all tracks for a given album.
382        # Mandatory only if you reported ARTIST_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_playlist_tracks(  # type: ignore[empty-body]
389        self,
390        prov_playlist_id: str,
391        page: int = 0,
392    ) -> list[Track]:
393        """Get all playlist tracks for given playlist id."""
394        # Get all tracks for a given playlist.
395        # Mandatory only if you reported LIBRARY_PLAYLISTS in the supported_features.
396        # NOTE: It is advised to apply caching here (if possible)
397        # to avoid too many calls to the provider's API.
398        # You can use the @use_cache decorator from music_assistant.controllers.cache
399        # to easily apply caching to this method.
400
401    async def library_add(self, item: MediaItemType) -> bool:
402        """Add item to provider's library. Return true on success."""
403        # Add an item to your provider's library.
404        # This is only called if the provider supports the EDIT feature for the media type.
405        return True
406
407    async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool:
408        """Remove item from provider's library. Return true on success."""
409        # Remove an item from your provider's library.
410        # This is only called if the provider supports the EDIT feature for the media type.
411        return True
412
413    async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None:
414        """Add track(s) to playlist."""
415        # Add track(s) to a playlist.
416        # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature.
417
418    async def remove_playlist_tracks(
419        self, prov_playlist_id: str, positions_to_remove: tuple[int, ...]
420    ) -> None:
421        """Remove track(s) from playlist."""
422        # Remove track(s) from a playlist.
423        # This is only called if the provider supports the PLAYLIST_TRACKS_EDIT feature.
424
425    async def create_playlist(self, name: str) -> Playlist:  # type: ignore[empty-body]
426        """Create a new playlist on provider with given name."""
427        # Create a new playlist on the provider.
428        # This is only called if the provider supports the PLAYLIST_CREATE feature.
429
430    async def get_similar_tracks(  # type: ignore[empty-body]
431        self, prov_track_id: str, limit: int = 25
432    ) -> list[Track]:
433        """Retrieve a dynamic list of similar tracks based on the provided track."""
434        # Get a list of similar tracks based on the provided track.
435        # This is only called if the provider supports the SIMILAR_TRACKS feature.
436        # NOTE: It is advised to apply caching here (if possible)
437        # to avoid too many calls to the provider's API.
438        # You can use the @use_cache decorator from music_assistant.controllers.cache
439        # to easily apply caching to this method.
440
441    async def get_resume_position(self, item_id: str, media_type: MediaType) -> tuple[bool, int]:  # type: ignore[empty-body]
442        """
443        Get progress (resume point) details for the given Audiobook or Podcast episode.
444
445        This is a separate call from the regular get_item call to ensure the resume position
446        is always up-to-date and because a lot providers have this info present on a dedicated
447        endpoint.
448
449        Will be called right before playback starts to ensure the resume position is correct.
450
451        Returns a boolean with the fully_played status
452        and an integer with the resume position in ms.
453        """
454        # optional function to get the resume position of a audiobook or podcast episode
455        # only implement this if your provider supports providing this information!
456
457    async def get_stream_details(self, item_id: str, media_type: MediaType) -> StreamDetails:
458        """Get streamdetails for a track/radio."""
459        # Get stream details for a track or radio.
460        # Implementing this method is MANDATORY to allow playback.
461        # The StreamDetails contain info how Music Assistant can play the track.
462        # item_id will always be a track or radio id. Later, when/if MA supports
463        # podcasts or audiobooks, this may as well be an episode or chapter id.
464        # You should return a StreamDetails object here with the info as accurate as possible
465        # to allow Music Assistant to process the audio using ffmpeg.
466        return StreamDetails(
467            provider=self.instance_id,
468            item_id=item_id,
469            audio_format=AudioFormat(
470                # provide details here about sample rate etc. if known
471                # set content type to unknown to let ffmpeg guess the codec/container
472                content_type=ContentType.UNKNOWN,
473            ),
474            media_type=MediaType.TRACK,
475            # streamtype defines how the stream is provided
476            # for most providers this will be HTTP but you can also use CUSTOM
477            # to provide a custom stream generator in get_audio_stream.
478            stream_type=StreamType.HTTP,
479            # explore the StreamDetails model and StreamType enum for more options
480            # but the above should be the mandatory fields to set.
481            allow_seek=True,
482            # set allow_seek to True if the stream may be seeked
483            can_seek=True,
484            # set can_seek to True if the stream supports seeking
485        )
486
487    async def get_audio_stream(
488        self, streamdetails: StreamDetails, seek_position: int = 0
489    ) -> AsyncGenerator[bytes, None]:
490        """
491        Return the (custom) audio stream for the provider item.
492
493        Will only be called when the stream_type is set to CUSTOM.
494        """
495        # this is an async generator that should yield raw audio bytes
496        # for the given streamdetails. You can use this to provide a custom
497        # stream generator for the audio stream. This is only called when the
498        # stream_type is set to CUSTOM in the get_stream_details method.
499        yield  # type: ignore[misc]
500
501    async def on_streamed(
502        self,
503        streamdetails: StreamDetails,
504    ) -> None:
505        """
506        Handle callback when given streamdetails completed streaming.
507
508        To get the number of seconds streamed, see streamdetails.seconds_streamed.
509        To get the number of seconds seeked/skipped, see streamdetails.seek_position.
510        Note that seconds_streamed is the total streamed seconds, so without seeked time.
511
512        NOTE: Due to internal and player buffering,
513        this may be called in advance of the actual completion.
514        """
515        # This is an OPTIONAL callback that is called when an item has been streamed.
516        # You can use this e.g. for playback reporting or statistics.
517
518    async def on_played(
519        self,
520        media_type: MediaType,
521        prov_item_id: str,
522        fully_played: bool,
523        position: int,
524        media_item: MediaItemType,
525        is_playing: bool = False,
526    ) -> None:
527        """
528        Handle callback when a (playable) media item has been played.
529
530        This is called by the Queue controller when;
531            - a track has been fully played
532            - a track has been stopped (or skipped) after being played
533            - every 30s when a track is playing
534
535        Fully played is True when the track has been played to the end.
536
537        Position is the last known position of the track in seconds, to sync resume state.
538        When fully_played is set to false and position is 0,
539        the user marked the item as unplayed in the UI.
540
541        is_playing is True when the track is currently playing.
542
543        media_item is the full media item details of the played/playing track.
544        """
545        # This is an OPTIONAL callback that is called when an item has been streamed.
546        # You can use this e.g. for playback reporting or statistics.
547
548    async def resolve_image(self, path: str) -> str | bytes:
549        """
550        Resolve an image from an image path.
551
552        This either returns (a generator to get) raw bytes of the image or
553        a string with an http(s) URL or local path that is accessible from the server.
554        """
555        # This is an OPTIONAL method that you can implement to resolve image paths.
556        # This is used to resolve image paths that are returned in the MediaItems.
557        # You can return a URL to an image or a generator that yields the raw bytes of the image.
558        # This will only be called when you set 'remotely_accessible'
559        # to false in a MediaItemImage object.
560        return path
561
562    async def browse(self, path: str) -> Sequence[MediaItemType | ItemMapping | BrowseFolder]:
563        """Browse this provider's items.
564
565        :param path: The path to browse, (e.g. provider_id://artists).
566        """
567        # Browse your provider's recommendations/media items.
568        # This is only called if you reported the BROWSE feature in the supported_features.
569        # You should return a list of MediaItems or ItemMappings for the given path.
570        # Note that you can return nested levels with BrowseFolder items.
571
572        # The MusicProvider base model has a default implementation of this method
573        # that will call the get_library_* methods if you did not override it.
574        return []
575
576    async def recommendations(self) -> list[RecommendationFolder]:
577        """
578        Get this provider's recommendations.
579
580        Returns an actual (and often personalised) list of recommendations
581        from this provider for the user/account.
582        """
583        # Get this provider's recommendations.
584        # This is only called if you reported the RECOMMENDATIONS feature in the supported_features.
585        return []
586
587    async def sync_library(self, media_type: MediaType) -> None:
588        """Run library sync for this provider."""
589        # Run a full sync of the library for the given media type.
590        # This is called by the music controller to sync items from your provider to the library.
591        # As a generic rule of thumb the default implementation within the MusicProvider
592        # base model should be sufficient for most (streaming) providers.
593        # If you need to do some custom sync logic, you can override this method.
594        # For example the filesystem provider in MA, overrides this method to scan the filesystem.
595