/
/
/
1"""
2DEMO/TEMPLATE Plugin Provider for Music Assistant.
3
4This is an empty plugin provider with no actual implementation.
5Its meant to get started developing a new plugin 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 plugin providers to get a better understanding.
9
10In general, a plugin provider does not have any mandatory implementation details.
11It provides additional functionality to Music Assistant and most often it will
12interact with the existing core controllers and event logic. For example a Scrobble plugin.
13
14If your plugin needs to communicate with external services or devices, you need to
15use a dedicated (async) library for that. You can add these dependencies to the
16manifest.json file in the requirements section,
17which is a list of (versioned!) python modules (pip syntax) that should be installed
18when the provider is selected by the user.
19
20To add a new plugin provider to Music Assistant, you need to create a new folder
21in the providers folder with the name of your provider (e.g. 'my_plugin_provider').
22In that folder you should create (at least) a __init__.py file and a manifest.json file.
23
24Optional is an icon.svg file that will be used as the icon for the provider in the UI,
25but we also support that you specify a material design icon in the manifest.json file.
26
27IMPORTANT NOTE:
28We strongly recommend developing on either macOS or Linux and start your development
29environment by running the setup.sh scripts in the scripts folder of the repository.
30This will create a virtual environment and install all dependencies needed for development.
31See also our general DEVELOPMENT.md guide in the repository for more information.
32
33"""
34
35from __future__ import annotations
36
37from collections.abc import AsyncGenerator
38from typing import TYPE_CHECKING
39
40from music_assistant_models.enums import ContentType, EventType, ProviderFeature
41from music_assistant_models.media_items.audio_format import AudioFormat
42
43from music_assistant.models.plugin import PluginProvider, PluginSource
44
45if TYPE_CHECKING:
46 from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig
47 from music_assistant_models.event import MassEvent
48 from music_assistant_models.provider import ProviderManifest
49
50 from music_assistant.mass import MusicAssistant
51 from music_assistant.models import ProviderInstanceType
52
53SUPPORTED_FEATURES = {
54 # MANDATORY
55 # this constant should contain a set of provider-level features
56 # that your provider supports or an empty set if none.
57 # see the ProviderFeature enum for all available features
58 # at time of writing the only plugin-specific feature is the
59 # 'AUDIO_SOURCE' feature which indicates that this provider can
60 # provide a (single) audio source to Music Assistant, such as a live stream.
61 # we add this feature here to demonstrate the concept.
62 ProviderFeature.AUDIO_SOURCE
63}
64
65
66async def setup(
67 mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
68) -> ProviderInstanceType:
69 """Initialize provider(instance) with given configuration."""
70 # setup is called when the user wants to setup a new provider instance.
71 # you are free to do any preflight checks here and but you must return
72 # an instance of the provider.
73 return MyDemoPluginprovider(mass, manifest, config, SUPPORTED_FEATURES)
74
75
76async def get_config_entries(
77 mass: MusicAssistant,
78 instance_id: str | None = None,
79 action: str | None = None,
80 values: dict[str, ConfigValueType] | None = None,
81) -> tuple[ConfigEntry, ...]:
82 """
83 Return Config entries to setup this provider.
84
85 instance_id: id of an existing provider instance (None if new instance setup).
86 action: [optional] action key called from config entries UI.
87 values: the (intermediate) raw values for config entries sent with the action.
88 """
89 # ruff: noqa: ARG001
90 # Config Entries are used to configure the Provider if needed.
91 # See the models of ConfigEntry and ConfigValueType for more information what is supported.
92 # The ConfigEntry is a dataclass that represents a single configuration entry.
93 # The ConfigValueType is an Enum that represents the type of value that
94 # can be stored in a ConfigEntry.
95 # If your provider does not need any configuration, you can return an empty tuple.
96 return ()
97
98
99class MyDemoPluginprovider(PluginProvider):
100 """
101 Example/demo Plugin provider.
102
103 Note that this is always subclassed from PluginProvider,
104 which in turn is a subclass of the generic Provider model.
105
106 The base implementation already takes care of some convenience methods,
107 such as the mass object and the logger. Take a look at the base class
108 for more information on what is available.
109
110 Just like with any other subclass, make sure that if you override
111 any of the default methods (such as __init__), you call the super() method.
112 In most cases its not needed to override any of the builtin methods and you only
113 implement the abc methods with your actual implementation.
114 """
115
116 async def loaded_in_mass(self) -> None:
117 """Call after the provider has been loaded."""
118 # OPTIONAL
119 # this is an optional method that you can implement if
120 # relevant or leave out completely if not needed.
121 # it will be called after the provider has been fully loaded into Music Assistant.
122 # you can use this for instance to trigger custom (non-mdns) discovery of plugins
123 # or any other logic that needs to run after the provider is fully loaded.
124
125 # as reference we will subscribe here to an event on the MA eventbus
126 # this is just an example and you can remove this if not needed.
127 async def handle_event(event: MassEvent) -> None:
128 if event.event == EventType.MEDIA_ITEM_PLAYED:
129 # example implementation of handling a media item played event
130 self.logger.info("Media item played event received: %s", event.data)
131
132 self.mass.subscribe(handle_event, EventType.MEDIA_ITEM_PLAYED)
133
134 async def unload(self, is_removed: bool = False) -> None:
135 """
136 Handle unload/close of the provider.
137
138 Called when provider is deregistered (e.g. MA exiting or config reloading).
139 is_removed will be set to True when the provider is removed from the configuration.
140 """
141 # OPTIONAL
142 # this is an optional method that you can implement if
143 # relevant or leave out completely if not needed.
144 # it will be called when the provider is unloaded from Music Assistant.
145 # this means also when the provider is getting reloaded
146
147 def get_source(self) -> PluginSource:
148 """Get (audio)source details for this plugin."""
149 # OPTIONAL
150 # Will only be called if ProviderFeature.AUDIO_SOURCE is declared
151 # you return a PluginSource object that represents the audio source
152 # that this plugin provider provides.
153 # the audio_format field should be the native audio format of the stream
154 # that is returned by the get_audio_stream method.
155 return PluginSource(
156 id=self.instance_id,
157 name=self.name,
158 passive=False,
159 can_play_pause=False,
160 can_seek=False,
161 audio_format=AudioFormat(content_type=ContentType.MP3),
162 )
163
164 async def get_audio_stream(self, player_id: str) -> AsyncGenerator[bytes, None]:
165 """
166 Return the (custom) audio stream for the audio source provided by this plugin.
167
168 Will only be called if this plugin is a PLuginSource, meaning that
169 the ProviderFeature.AUDIO_SOURCE is declared.
170
171 The player_id is the id of the player that is requesting the stream.
172 """
173 # OPTIONAL
174 # Will only be called if ProviderFeature.AUDIO_SOURCE is declared
175 # This will be called when this pluginsource has been selected by the user
176 # to play on one of the players.
177
178 # you should return an async generator that yields the audio stream data.
179 # this is an example implementation that just yields some dummy data
180 # you should replace this with your actual implementation.
181 for _ in range(100):
182 yield b"dummy audio data"
183