Fix race conditions when loading providers
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 19 Feb 2024 18:35:16 +0000 (19:35 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 19 Feb 2024 18:35:16 +0000 (19:35 +0100)
31 files changed:
music_assistant/server/models/provider.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/chromecast/__init__.py
music_assistant/server/providers/deezer/__init__.py
music_assistant/server/providers/dlna/__init__.py
music_assistant/server/providers/fanarttv/__init__.py
music_assistant/server/providers/filesystem_local/__init__.py
music_assistant/server/providers/filesystem_local/base.py
music_assistant/server/providers/filesystem_smb/__init__.py
music_assistant/server/providers/fully_kiosk/__init__.py
music_assistant/server/providers/hass/__init__.py
music_assistant/server/providers/hass_players/__init__.py
music_assistant/server/providers/jellyfin/__init__.py
music_assistant/server/providers/musicbrainz/__init__.py
music_assistant/server/providers/opensubsonic/__init__.py
music_assistant/server/providers/opensubsonic/sonic_provider.py
music_assistant/server/providers/plex/__init__.py
music_assistant/server/providers/qobuz/__init__.py
music_assistant/server/providers/radiobrowser/__init__.py
music_assistant/server/providers/slimproto/__init__.py
music_assistant/server/providers/snapcast/__init__.py
music_assistant/server/providers/sonos/__init__.py
music_assistant/server/providers/soundcloud/__init__.py
music_assistant/server/providers/spotify/__init__.py
music_assistant/server/providers/theaudiodb/__init__.py
music_assistant/server/providers/tidal/__init__.py
music_assistant/server/providers/tunein/__init__.py
music_assistant/server/providers/ugp/__init__.py
music_assistant/server/providers/url/__init__.py
music_assistant/server/providers/ytmusic/__init__.py
music_assistant/server/server.py

index e31677503ccbdd342644f645fe3dd04195d491ac..9ae02459ba1a59a3819846fb0de8d9ed386e5fe7 100644 (file)
@@ -51,8 +51,8 @@ class Provider:
         """Return the features supported by this Provider."""
         return ()
 
-    async def handle_setup(self) -> None:
-        """Handle async initialization of the provider."""
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
 
     async def unload(self) -> None:
         """
index 000e7f9c1b48ee2da3f4b57ab7a94ce747a6e8ce..2773a40c01d1e539551ded281ab792ecb2871ac8 100644 (file)
@@ -132,7 +132,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = AirplayProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -492,12 +492,11 @@ class AirplayProvider(PlayerProvider):
         """Return the features supported by this Provider."""
         return (ProviderFeature.SYNC_PLAYERS,)
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._atv_players = {}
         self._stream_tasks = {}
         self._cliraop_bin = await self.get_cliraop_binary()
-        self.mass.create_task(self._run_discovery())
         dacp_port = await select_free_port(39831, 49831)
         # the pyatv logger is way to noisy, silence it a bit
         logging.getLogger("pyatv").setLevel(self.logger.level + 10)
@@ -523,6 +522,10 @@ class AirplayProvider(PlayerProvider):
         )
         await self.mass.zeroconf.async_register_service(self._dacp_info)
 
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        await self._run_discovery()
+
     async def unload(self) -> None:
         """Handle close/cleanup of the provider."""
         # power off all players (will disconnct and close cliraop)
index fa300edf9a0f4b95e5aac94dd8fd40374f00c3b3..4e3d6a6db418351ee0daeb3cc4e39b9baef2e2b5 100644 (file)
@@ -90,9 +90,7 @@ async def setup(
     mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
-    prov = ChromecastProvider(mass, manifest, config)
-    await prov.handle_setup()
-    return prov
+    return ChromecastProvider(mass, manifest, config)
 
 
 async def get_config_entries(
@@ -135,8 +133,11 @@ class ChromecastProvider(PlayerProvider):
     castplayers: dict[str, CastPlayer]
     _discover_lock: threading.Lock
 
-    async def handle_setup(self) -> None:
+    def __init__(
+        self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+    ) -> None:
         """Handle async initialization of the provider."""
+        super.__init__(mass, manifest, config)
         self._discover_lock = threading.Lock()
         self.castplayers = {}
         self.mz_mgr = MultizoneManager()
@@ -150,6 +151,9 @@ class ChromecastProvider(PlayerProvider):
         )
         # silence pychromecast logging
         logging.getLogger("pychromecast").setLevel(self.logger.level + 10)
+
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
         # start discovery in executor
         await self.mass.loop.run_in_executor(None, self.browser.start_discovery)
 
index eaf572d86746e921607b40addc63bc84bc615814..be7687e82c1278243910571f99ce3b5f9db1c3bb 100644 (file)
@@ -124,7 +124,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = DeezerProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -176,8 +176,8 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
     credentials: DeezerCredentials
     user: deezer.User
 
-    async def handle_setup(self) -> None:
-        """Set up the Deezer provider."""
+    async def handle_async_init(self) -> None:
+        """Handle async init of the Deezer provider."""
         self.credentials = DeezerCredentials(
             app_id=DEEZER_APP_ID,
             app_secret=DEEZER_APP_SECRET,
index 0dab2ddba8ae3c20bb9fc5f7cbd4b8a2d8a55924..83924d9b26ba0d3178849c01cdf5cb6f122b9da0 100644 (file)
@@ -113,7 +113,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = DLNAPlayerProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -277,7 +277,7 @@ class DLNAPlayerProvider(PlayerProvider):
     upnp_factory: UpnpFactory
     notify_server: DLNANotifyServer
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.dlnaplayers = {}
         self.lock = asyncio.Lock()
@@ -286,7 +286,10 @@ class DLNAPlayerProvider(PlayerProvider):
         self.requester = AiohttpSessionRequester(self.mass.http_session, with_sleep=True)
         self.upnp_factory = UpnpFactory(self.requester, non_strict=True)
         self.notify_server = DLNANotifyServer(self.requester, self.mass)
-        self.mass.create_task(self._run_discovery())
+
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        await self._run_discovery()
 
     async def unload(self) -> None:
         """
index 24a13b9114e17ec1c3012c5988b95acd6ec4955c..c6455e292be7313d67c260ffd18c71ec538be8ea 100644 (file)
@@ -46,7 +46,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = FanartTvMetadataProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -72,7 +72,7 @@ class FanartTvMetadataProvider(MetadataProvider):
 
     throttler: Throttler
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.cache = self.mass.cache
         self.throttler = Throttler(rate_limit=2, period=1)
index 8fcce0606e3696316e6867e76ccea9ecf362fd2c..5ae0a780678cff02121e4347fb9541eab9ffe6b2 100644 (file)
@@ -48,7 +48,7 @@ async def setup(
         msg = f"Music Directory {conf_path} does not exist"
         raise SetupFailedError(msg)
     prov = LocalFileSystemProvider(mass, manifest, config)
-    await prov.handle_setup()
+    prov.base_path = config.get_value(CONF_PATH)
     return prov
 
 
@@ -99,10 +99,6 @@ class LocalFileSystemProvider(FileSystemProviderBase):
 
     base_path: str
 
-    async def handle_setup(self) -> None:
-        """Handle async initialization of the provider."""
-        self.base_path = self.config.get_value(CONF_PATH)
-
     async def listdir(
         self, path: str, recursive: bool = False
     ) -> AsyncGenerator[FileSystemItem, None]:
index e2c37f8986d6519ed497699f3c3c33c46f5350b0..a764a19339ec253e364f84b16c3ec37991c9dce4 100644 (file)
@@ -12,10 +12,7 @@ from typing import TYPE_CHECKING
 import cchardet
 import xmltodict
 
-from music_assistant.common.helpers.util import (
-    create_sort_name,
-    parse_title_and_version,
-)
+from music_assistant.common.helpers.util import create_sort_name, parse_title_and_version
 from music_assistant.common.models.config_entries import (
     ConfigEntry,
     ConfigEntryType,
@@ -158,7 +155,7 @@ class FileSystemProviderBase(MusicProvider):
         return SUPPORTED_FEATURES
 
     @abstractmethod
-    async def handle_setup(self) -> None:
+    async def async_setup(self) -> None:
         """Handle async initialization of the provider."""
 
     @abstractmethod
index a5d1c7cc777e6ec1974e4fc1e4879b3422651ea8..d61f35dbe4f640dc0c34c16fecbeb4c2b1d2e150 100644 (file)
@@ -45,7 +45,7 @@ async def setup(
         msg = "Invalid share name"
         raise LoginFailed(msg)
     prov = SMBFileSystemProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -131,7 +131,7 @@ class SMBFileSystemProvider(LocalFileSystemProvider):
     smb library for Python (and we tried both pysmb and smbprotocol).
     """
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         # base_path will be the path where we're going to mount the remote share
         self.base_path = f"/tmp/{self.instance_id}"  # noqa: S108
index 6c2e662b78ac95825ec3f8ed1aecea6094f368b3..c505618c2b6fc80e20c85cff8c552141afdd19ee 100644 (file)
@@ -43,7 +43,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = FullyKioskProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -90,7 +90,7 @@ class FullyKioskProvider(PlayerProvider):
 
     _fully: FullyKiosk
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._fully = FullyKiosk(
             self.mass.http_session,
@@ -101,14 +101,13 @@ class FullyKioskProvider(PlayerProvider):
         try:
             async with asyncio.timeout(15):
                 await self._fully.getDeviceInfo()
-                self._handle_player_init()
-                self._handle_player_update()
         except Exception as err:
             msg = f"Unable to start the FullyKiosk connection ({err!s}"
             raise SetupFailedError(msg) from err
 
-    def _handle_player_init(self) -> None:
-        """Process FullyKiosk add to Player controller."""
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        # Add FullyKiosk device to Player controller.
         player_id = self._fully.deviceInfo["deviceID"]
         player = self.mass.players.get(player_id, raise_unavailable=False)
         address = (
@@ -130,6 +129,7 @@ class FullyKioskProvider(PlayerProvider):
                 supported_features=(PlayerFeature.VOLUME_SET,),
             )
         self.mass.players.register_or_update(player)
+        self._handle_player_update()
 
     def _handle_player_update(self) -> None:
         """Update FullyKiosk player attributes."""
index 486934dda6df936f1c1bd74d4a7381a802b52ef6..60f527cbe1dfdab460c8bc634915756a9e6ef4ea 100644 (file)
@@ -47,7 +47,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = HomeAssistant(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -153,11 +153,11 @@ class HomeAssistant(PluginProvider):
 
     hass: HomeAssistantClient
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the plugin."""
         url = get_websocket_url(self.config.get_value(CONF_URL))
         token = self.config.get_value(CONF_AUTH_TOKEN)
-        logging.getLogger("hass_client").setLevel(self.logger.level)
+        logging.getLogger("hass_client").setLevel(self.logger.level + 10)
         self.hass = HomeAssistantClient(url, token, self.mass.http_session)
         await self.hass.connect()
 
index 9767c59544750586cbeb4a76c07b2bafa504b5a4..9d611a8a424b9dc2af67a876f63449114786f9f1 100644 (file)
@@ -142,8 +142,12 @@ async def setup(
     mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
+    hass_prov: HomeAssistantProvider = mass.get_provider(HASS_DOMAIN)
+    if not hass_prov:
+        msg = "The Home Assistant Plugin needs to be set-up first"
+        raise SetupFailedError(msg)
     prov = HomeAssistantPlayers(mass, manifest, config)
-    await prov.handle_setup()
+    prov.hass_prov = hass_prov
     return prov
 
 
@@ -185,24 +189,21 @@ class HomeAssistantPlayers(PlayerProvider):
 
     hass_prov: HomeAssistantProvider
 
-    async def handle_setup(self) -> None:
-        """Handle async initialization of the plugin."""
-        hass_prov: HomeAssistantProvider = self.mass.get_provider(HASS_DOMAIN)
-        if not hass_prov:
-            msg = "The Home Assistant Plugin needs to be set-up first"
-            raise SetupFailedError(msg)
-        self.hass_prov = hass_prov
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
         player_ids: list[str] = self.config.get_value(CONF_PLAYERS)
         # prefetch the device- and entity registry
-        device_registry = {x["id"]: x for x in await hass_prov.hass.get_device_registry()}
-        entity_registry = {x["entity_id"]: x for x in await hass_prov.hass.get_entity_registry()}
+        device_registry = {x["id"]: x for x in await self.hass_prov.hass.get_device_registry()}
+        entity_registry = {
+            x["entity_id"]: x for x in await self.hass_prov.hass.get_entity_registry()
+        }
         # setup players from hass entities
-        async for state in _get_hass_media_players(hass_prov):
+        async for state in _get_hass_media_players(self.hass_prov):
             if state["entity_id"] not in player_ids:
                 continue
             await self._setup_player(state, entity_registry, device_registry)
         # register for entity state updates
-        await hass_prov.hass.subscribe_entities(self._on_entity_state_update, player_ids)
+        await self.hass_prov.hass.subscribe_entities(self._on_entity_state_update, player_ids)
         # remove any leftover players (after reconfigure of players)
         for player in self.players:
             if player.player_id not in player_ids:
index b28380c732911a6e909a535e2b3d3bfe5c9ca8dc..085fbc8354be4495d538331d050f78863e10f2b6 100644 (file)
@@ -109,7 +109,7 @@ async def setup(
 ) -> ProviderInstanceType:\r
     """Initialize provider(instance) with given configuration."""\r
     prov = JellyfinProvider(mass, manifest, config)\r
-    await prov.handle_setup()\r
+    await prov.handle_async_init()\r
     return prov\r
 \r
 \r
@@ -159,7 +159,7 @@ class JellyfinProvider(MusicProvider):
 \r
     # _jellyfin_server : JellyfinClient = None\r
 \r
-    async def handle_setup(self) -> None:\r
+    async def handle_async_init(self) -> None:\r
         """Initialize provider(instance) with given configuration."""\r
         logging.getLogger("pytube").setLevel(self.logger.level + 10)\r
         logging.getLogger("ytmusicapi").setLevel(self.logger.level + 10)\r
index d39ee290545dad5d38665bd54aa81dc55a2664f0..11d8398b8cfa67ed766cad2fa03f1339f6ab04b2 100644 (file)
@@ -47,7 +47,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = MusicbrainzProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -203,7 +203,7 @@ class MusicbrainzProvider(MetadataProvider):
 
     throttler: Throttler
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.cache = self.mass.cache
         self.throttler = Throttler(rate_limit=1, period=1)
index 89226a195baf28c592e740e29b6c3c3fa4640e58..efbbcd6982e4d4c91692e918822318e64351c6d3 100644 (file)
@@ -30,7 +30,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = OpenSonicProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
index ff48d471eed17df0135f07c2e14eb6f5bb51e6cd..bc77f4ca8da5b7e2ee5cb55e5d10b0aef642a888 100644 (file)
@@ -66,7 +66,7 @@ class OpenSonicProvider(MusicProvider):
     _conn: SonicConnection = None
     _enable_podcasts: bool = True
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Set up the music provider and test the connection."""
         logging.getLogger("libopensonic").setLevel(self.logger.level)
         port = self.config.get_value(CONF_PORT)
index c3555cc7875f4507cdc48571155ec51f5b0ede6b..8d714db05660a80153c571c798b64cd1759e095e 100644 (file)
@@ -83,7 +83,7 @@ async def setup(
         raise LoginFailed(msg)
 
     prov = PlexProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -199,7 +199,7 @@ class PlexProvider(MusicProvider):
     _plex_library: PlexMusicSection = None
     _myplex_account: MyPlexAccount = None
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Set up the music provider by connecting to the server."""
         # silence loggers
         logging.getLogger("plexapi").setLevel(self.logger.level + 10)
index 44a60182a78df42331d48d6a6b4c6312d86dcbe9..b0a12cea9f302847c2bf4ce959b0b0fdba609423 100644 (file)
@@ -13,11 +13,7 @@ from asyncio_throttle import Throttler
 
 from music_assistant.common.helpers.util import parse_title_and_version, try_parse_int
 from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
-from music_assistant.common.models.enums import (
-    ConfigEntryType,
-    ExternalID,
-    ProviderFeature,
-)
+from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature
 from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError
 from music_assistant.common.models.media_items import (
     Album,
@@ -82,7 +78,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = QobuzProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -122,7 +118,7 @@ class QobuzProvider(MusicProvider):
     _user_auth_info: str | None = None
     _throttler: Throttler
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._throttler = Throttler(rate_limit=4, period=1)
 
index 3a8d779dd5beef14e5a176fd9ea4042c6ae0e19a..a059e2b809a250fbdf1402da512b6ad023574f32 100644 (file)
@@ -46,7 +46,7 @@ async def setup(
     """Initialize provider(instance) with given configuration."""
     prov = RadioBrowserProvider(mass, manifest, config)
 
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -74,7 +74,7 @@ class RadioBrowserProvider(MusicProvider):
         """Return the features supported by this Provider."""
         return SUPPORTED_FEATURES
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.radios = RadioBrowser(
             session=self.mass.http_session, user_agent=f"MusicAssistant/{self.mass.version}"
index 5a5e0d727f961e02aa4815aae38998e07df423e3..bbf9388fe49abb24d4252cc8d72c1a74445ade31 100644 (file)
@@ -104,7 +104,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = SlimprotoProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -199,7 +199,7 @@ class SlimprotoProvider(PlayerProvider):
         """Return the features supported by this Provider."""
         return (ProviderFeature.SYNC_PLAYERS,)
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._socket_clients = {}
         self._sync_playpoints = {}
index edb8df36f5ae99376f07bcfdda73bcb397de6a7d..13b0900e82f1b8e61809aedef17a9863febcd2a3 100644 (file)
@@ -57,7 +57,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = SnapCastProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -106,7 +106,7 @@ class SnapCastProvider(PlayerProvider):
         """Return the features supported by this Provider."""
         return (ProviderFeature.SYNC_PLAYERS,)
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.snapcast_server_host = self.config.get_value(CONF_SNAPCAST_SERVER_HOST)
         self.snapcast_server_control_port = self.config.get_value(CONF_SNAPCAST_SERVER_CONTROL_PORT)
@@ -119,7 +119,6 @@ class SnapCastProvider(PlayerProvider):
                 reconnect=True,
             )
             self._snapserver.set_on_update_callback(self._handle_update)
-            self._handle_update()
             self.logger.info(
                 f"Started Snapserver connection on:"
                 f"{self.snapcast_server_host}:{self.snapcast_server_control_port}"
@@ -128,6 +127,17 @@ class SnapCastProvider(PlayerProvider):
             msg = "Unable to start the Snapserver connection ?"
             raise SetupFailedError(msg) from err
 
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        # initial load of players
+        self._handle_update()
+
+    async def unload(self) -> None:
+        """Handle close/cleanup of the provider."""
+        for client in self._snapserver.clients:
+            await self.cmd_stop(client.identifier)
+        await self._snapserver.stop()
+
     def _handle_update(self) -> None:
         """Process Snapcast init Player/Group and set callback ."""
         for snap_client in self._snapserver.clients:
@@ -190,12 +200,6 @@ class SnapCastProvider(PlayerProvider):
             player.active_source = stream.name
         self.mass.players.register_or_update(player)
 
-    async def unload(self) -> None:
-        """Handle close/cleanup of the provider."""
-        for client in self._snapserver.clients:
-            await self.cmd_stop(client.identifier)
-        await self._snapserver.stop()
-
     async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
         base_entries = await super().get_player_config_entries(player_id)
index b553b61d27fdc38f90c167786dac83bb8b13b00e..b80e00f8693b389c6ccdcae7b3fcba9fb7392efa 100644 (file)
@@ -94,7 +94,7 @@ async def setup(
     logging.getLogger("soco").setLevel(logging.INFO)
     logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO)
     prov = SonosPlayerProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -144,7 +144,7 @@ class SonosPlayerProvider(PlayerProvider):
         """Return the features supported by this Provider."""
         return (ProviderFeature.SYNC_PLAYERS,)
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.sonosplayers: OrderedDict[str, SonosPlayer] = OrderedDict()
         self.topology_condition = asyncio.Condition()
@@ -157,7 +157,9 @@ class SonosPlayerProvider(PlayerProvider):
         self.creation_lock = asyncio.Lock()
         self._known_invisible: set[SoCo] = set()
 
-        self.mass.create_task(self._run_discovery())
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        await self._run_discovery()
 
     async def unload(self) -> None:
         """Handle close/cleanup of the provider."""
index a8c4df7dadab1d88f63269f020f2ca2a8fd905fe..12b67882386eafe20e4fc9d9b092b6a0eb357341 100644 (file)
@@ -59,7 +59,7 @@ async def setup(
         msg = "Invalid login credentials"
         raise LoginFailed(msg)
     prov = SoundcloudMusicProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -105,7 +105,7 @@ class SoundcloudMusicProvider(MusicProvider):
     _soundcloud = None
     _me = None
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Set up the Soundcloud provider."""
         client_id = self.config.get_value(CONF_CLIENT_ID)
         auth_token = self.config.get_value(CONF_AUTHORIZATION)
index 0f3eb78f60edc206f0417a12f03fc5615199fd07..0a4edcaf07e60eec3659c7d5913b83c07f892f64 100644 (file)
@@ -17,11 +17,7 @@ from asyncio_throttle import Throttler
 
 from music_assistant.common.helpers.util import parse_title_and_version
 from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
-from music_assistant.common.models.enums import (
-    ConfigEntryType,
-    ExternalID,
-    ProviderFeature,
-)
+from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature
 from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError
 from music_assistant.common.models.media_items import (
     Album,
@@ -82,7 +78,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = SpotifyProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -123,12 +119,11 @@ class SpotifyProvider(MusicProvider):
     _sp_user: str | None = None
     _librespot_bin: str | None = None
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._throttler = Throttler(rate_limit=1, period=0.1)
         self._cache_dir = CACHE_DIR
         self._ap_workaround = False
-
         # try to get a token, raise if that fails
         self._cache_dir = os.path.join(CACHE_DIR, self.instance_id)
         # try login which will raise if it fails
index b7dc8001ab95d58dfb7780c34f25336f3086e60e..68d19bc516c8344c57f4eefe097d838d7fc4bcfb 100644 (file)
@@ -80,7 +80,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = AudioDbMetadataProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -106,7 +106,7 @@ class AudioDbMetadataProvider(MetadataProvider):
 
     throttler: Throttler
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self.cache = self.mass.cache
         self.throttler = Throttler(rate_limit=2, period=1)
index a476095bd538946424ee5a156cf946036f129dfc..15bde8fa0bebee32201650d4b87d8080a8431db3 100644 (file)
@@ -94,7 +94,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = TidalProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -211,7 +211,7 @@ class TidalProvider(MusicProvider):
     _tidal_session: TidalSession | None = None
     _tidal_user_id: str | None = None
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._tidal_user_id: str = self.config.get_value(CONF_USER_ID)
         self._tidal_session = await self._get_tidal_session()
index 05a414d3356da839a97684b3d277e74ebffce702..9494067a80036ad759223769f811567c8907395a 100644 (file)
@@ -10,11 +10,7 @@ from asyncio_throttle import Throttler
 from music_assistant.common.helpers.util import create_sort_name
 from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType
 from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature
-from music_assistant.common.models.errors import (
-    InvalidDataError,
-    LoginFailed,
-    MediaNotFoundError,
-)
+from music_assistant.common.models.errors import InvalidDataError, LoginFailed, MediaNotFoundError
 from music_assistant.common.models.media_items import (
     AudioFormat,
     ContentType,
@@ -58,7 +54,7 @@ async def setup(
             "Email address detected instead of username, "
             "it is advised to use the tunein username instead of email."
         )
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -96,7 +92,7 @@ class TuneInProvider(MusicProvider):
         """Return the features supported by this Provider."""
         return SUPPORTED_FEATURES
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Handle async initialization of the provider."""
         self._throttler = Throttler(rate_limit=1, period=1)
 
index 7e621f3aba3a9673c30157b203e4e8a2cfa387f8..a86c766082764d99fa100e56421426e0985c3876 100644 (file)
@@ -26,11 +26,7 @@ from music_assistant.common.models.enums import (
     ProviderFeature,
 )
 from music_assistant.common.models.player import DeviceInfo, Player
-from music_assistant.constants import (
-    CONF_CROSSFADE,
-    CONF_GROUP_MEMBERS,
-    SYNCGROUP_PREFIX,
-)
+from music_assistant.constants import CONF_CROSSFADE, CONF_GROUP_MEMBERS, SYNCGROUP_PREFIX
 from music_assistant.server.models.player_provider import PlayerProvider
 
 if TYPE_CHECKING:
@@ -52,9 +48,7 @@ async def setup(
     mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
-    prov = UniversalGroupProvider(mass, manifest, config)
-    await prov.handle_setup()
-    return prov
+    return UniversalGroupProvider(mass, manifest, config)
 
 
 async def get_config_entries(
@@ -84,10 +78,16 @@ class UniversalGroupProvider(PlayerProvider):
         """Return the features supported by this Provider."""
         return (ProviderFeature.PLAYER_GROUP_CREATE,)
 
-    async def handle_setup(self) -> None:
-        """Handle async initialization of the provider."""
+    def __init__(
+        self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+    ) -> None:
+        """Initialize MusicProvider."""
+        super().__init__(mass, manifest, config)
         self.prev_sync_leaders = {}
-        self.mass.loop.create_task(self._register_all_players())
+
+    async def loaded_in_mass(self) -> None:
+        """Call after the provider has been loaded."""
+        await self._register_all_players()
 
     async def get_player_config_entries(self, player_id: str) -> tuple[ConfigEntry]:
         """Return all (provider/player specific) Config Entries for the given player (if any)."""
index 38422d773574823e0f0ec468ffeacf2a1469104c..f9aeddb9755572032a51f16147494e06876e4998 100644 (file)
@@ -43,9 +43,7 @@ async def setup(
     mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
-    prov = URLProvider(mass, manifest, config)
-    await prov.handle_setup()
-    return prov
+    return URLProvider(mass, manifest, config)
 
 
 async def get_config_entries(
@@ -68,11 +66,11 @@ async def get_config_entries(
 class URLProvider(MusicProvider):
     """Music Provider for manual URL's/files added to the queue."""
 
-    async def handle_setup(self) -> None:
-        """Handle async initialization of the provider.
-
-        Called when provider is registered.
-        """
+    def __init__(
+        self, mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
+    ) -> None:
+        """Initialize MusicProvider."""
+        super().__init__(mass, manifest, config)
         self._full_url = {}
 
     async def get_track(self, prov_track_id: str) -> Track:
index 9f2508a97982cd165aca21c08a9c713414efe50c..c0f243ab883dc269f83f6e9bf0ba29c975aeaeef 100644 (file)
@@ -119,7 +119,7 @@ async def setup(
 ) -> ProviderInstanceType:
     """Initialize provider(instance) with given configuration."""
     prov = YoutubeMusicProvider(mass, manifest, config)
-    await prov.handle_setup()
+    await prov.handle_async_init()
     return prov
 
 
@@ -188,7 +188,7 @@ class YoutubeMusicProvider(MusicProvider):
     _signature_timestamp = 0
     _cipher = None
 
-    async def handle_setup(self) -> None:
+    async def handle_async_init(self) -> None:
         """Set up the YTMusic provider."""
         logging.getLogger("pytube").setLevel(self.logger.level + 10)
         if not self.config.get_value(CONF_AUTH_TOKEN):
index 8d3b648ce304aaa7b81863e47ea6734ef99e72c0..137b179e075acd513cfe82c894efe642ae0a16f3 100644 (file)
@@ -420,7 +420,7 @@ class MusicAssistant:
                 )
                 raise SetupFailedError(msg)
 
-        # try to load the module
+        # try to setup the module
         prov_mod = await get_provider_module(domain)
         try:
             async with asyncio.timeout(30):
@@ -436,6 +436,7 @@ class MusicAssistant:
         )
         provider.available = True
         self._providers[provider.instance_id] = provider
+        self.create_task(provider.loaded_in_mass())
         self.config.set(f"{CONF_PROVIDERS}/{conf.instance_id}/last_error", None)
         self.signal_event(EventType.PROVIDERS_UPDATED, data=self.get_providers())
         # if this is a music provider, start sync