/
/
/
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