From: Marcel van der Veldt Date: Mon, 19 Feb 2024 18:35:16 +0000 (+0100) Subject: Fix race conditions when loading providers X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=7c2daf84a00a0d88ddcd1cbe376f415b4ce109d2;p=music-assistant-server.git Fix race conditions when loading providers --- diff --git a/music_assistant/server/models/provider.py b/music_assistant/server/models/provider.py index e3167750..9ae02459 100644 --- a/music_assistant/server/models/provider.py +++ b/music_assistant/server/models/provider.py @@ -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: """ diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index 000e7f9c..2773a40c 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -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) diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index fa300edf..4e3d6a6d 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -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) diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/server/providers/deezer/__init__.py index eaf572d8..be7687e8 100644 --- a/music_assistant/server/providers/deezer/__init__.py +++ b/music_assistant/server/providers/deezer/__init__.py @@ -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, diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index 0dab2ddb..83924d9b 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -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: """ diff --git a/music_assistant/server/providers/fanarttv/__init__.py b/music_assistant/server/providers/fanarttv/__init__.py index 24a13b91..c6455e29 100644 --- a/music_assistant/server/providers/fanarttv/__init__.py +++ b/music_assistant/server/providers/fanarttv/__init__.py @@ -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) diff --git a/music_assistant/server/providers/filesystem_local/__init__.py b/music_assistant/server/providers/filesystem_local/__init__.py index 8fcce060..5ae0a780 100644 --- a/music_assistant/server/providers/filesystem_local/__init__.py +++ b/music_assistant/server/providers/filesystem_local/__init__.py @@ -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]: diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py index e2c37f89..a764a193 100644 --- a/music_assistant/server/providers/filesystem_local/base.py +++ b/music_assistant/server/providers/filesystem_local/base.py @@ -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 diff --git a/music_assistant/server/providers/filesystem_smb/__init__.py b/music_assistant/server/providers/filesystem_smb/__init__.py index a5d1c7cc..d61f35db 100644 --- a/music_assistant/server/providers/filesystem_smb/__init__.py +++ b/music_assistant/server/providers/filesystem_smb/__init__.py @@ -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 diff --git a/music_assistant/server/providers/fully_kiosk/__init__.py b/music_assistant/server/providers/fully_kiosk/__init__.py index 6c2e662b..c505618c 100644 --- a/music_assistant/server/providers/fully_kiosk/__init__.py +++ b/music_assistant/server/providers/fully_kiosk/__init__.py @@ -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.""" diff --git a/music_assistant/server/providers/hass/__init__.py b/music_assistant/server/providers/hass/__init__.py index 486934dd..60f527cb 100644 --- a/music_assistant/server/providers/hass/__init__.py +++ b/music_assistant/server/providers/hass/__init__.py @@ -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() diff --git a/music_assistant/server/providers/hass_players/__init__.py b/music_assistant/server/providers/hass_players/__init__.py index 9767c595..9d611a8a 100644 --- a/music_assistant/server/providers/hass_players/__init__.py +++ b/music_assistant/server/providers/hass_players/__init__.py @@ -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: diff --git a/music_assistant/server/providers/jellyfin/__init__.py b/music_assistant/server/providers/jellyfin/__init__.py index b28380c7..085fbc83 100644 --- a/music_assistant/server/providers/jellyfin/__init__.py +++ b/music_assistant/server/providers/jellyfin/__init__.py @@ -109,7 +109,7 @@ async def setup( ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" prov = JellyfinProvider(mass, manifest, config) - await prov.handle_setup() + await prov.handle_async_init() return prov @@ -159,7 +159,7 @@ class JellyfinProvider(MusicProvider): # _jellyfin_server : JellyfinClient = None - async def handle_setup(self) -> None: + async def handle_async_init(self) -> None: """Initialize provider(instance) with given configuration.""" logging.getLogger("pytube").setLevel(self.logger.level + 10) logging.getLogger("ytmusicapi").setLevel(self.logger.level + 10) diff --git a/music_assistant/server/providers/musicbrainz/__init__.py b/music_assistant/server/providers/musicbrainz/__init__.py index d39ee290..11d8398b 100644 --- a/music_assistant/server/providers/musicbrainz/__init__.py +++ b/music_assistant/server/providers/musicbrainz/__init__.py @@ -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) diff --git a/music_assistant/server/providers/opensubsonic/__init__.py b/music_assistant/server/providers/opensubsonic/__init__.py index 89226a19..efbbcd69 100644 --- a/music_assistant/server/providers/opensubsonic/__init__.py +++ b/music_assistant/server/providers/opensubsonic/__init__.py @@ -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 diff --git a/music_assistant/server/providers/opensubsonic/sonic_provider.py b/music_assistant/server/providers/opensubsonic/sonic_provider.py index ff48d471..bc77f4ca 100644 --- a/music_assistant/server/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/server/providers/opensubsonic/sonic_provider.py @@ -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) diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py index c3555cc7..8d714db0 100644 --- a/music_assistant/server/providers/plex/__init__.py +++ b/music_assistant/server/providers/plex/__init__.py @@ -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) diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/server/providers/qobuz/__init__.py index 44a60182..b0a12cea 100644 --- a/music_assistant/server/providers/qobuz/__init__.py +++ b/music_assistant/server/providers/qobuz/__init__.py @@ -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) diff --git a/music_assistant/server/providers/radiobrowser/__init__.py b/music_assistant/server/providers/radiobrowser/__init__.py index 3a8d779d..a059e2b8 100644 --- a/music_assistant/server/providers/radiobrowser/__init__.py +++ b/music_assistant/server/providers/radiobrowser/__init__.py @@ -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}" diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index 5a5e0d72..bbf9388f 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -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 = {} diff --git a/music_assistant/server/providers/snapcast/__init__.py b/music_assistant/server/providers/snapcast/__init__.py index edb8df36..13b0900e 100644 --- a/music_assistant/server/providers/snapcast/__init__.py +++ b/music_assistant/server/providers/snapcast/__init__.py @@ -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) diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index b553b61d..b80e00f8 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -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.""" diff --git a/music_assistant/server/providers/soundcloud/__init__.py b/music_assistant/server/providers/soundcloud/__init__.py index a8c4df7d..12b67882 100644 --- a/music_assistant/server/providers/soundcloud/__init__.py +++ b/music_assistant/server/providers/soundcloud/__init__.py @@ -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) diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/server/providers/spotify/__init__.py index 0f3eb78f..0a4edcaf 100644 --- a/music_assistant/server/providers/spotify/__init__.py +++ b/music_assistant/server/providers/spotify/__init__.py @@ -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 diff --git a/music_assistant/server/providers/theaudiodb/__init__.py b/music_assistant/server/providers/theaudiodb/__init__.py index b7dc8001..68d19bc5 100644 --- a/music_assistant/server/providers/theaudiodb/__init__.py +++ b/music_assistant/server/providers/theaudiodb/__init__.py @@ -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) diff --git a/music_assistant/server/providers/tidal/__init__.py b/music_assistant/server/providers/tidal/__init__.py index a476095b..15bde8fa 100644 --- a/music_assistant/server/providers/tidal/__init__.py +++ b/music_assistant/server/providers/tidal/__init__.py @@ -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() diff --git a/music_assistant/server/providers/tunein/__init__.py b/music_assistant/server/providers/tunein/__init__.py index 05a414d3..9494067a 100644 --- a/music_assistant/server/providers/tunein/__init__.py +++ b/music_assistant/server/providers/tunein/__init__.py @@ -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) diff --git a/music_assistant/server/providers/ugp/__init__.py b/music_assistant/server/providers/ugp/__init__.py index 7e621f3a..a86c7660 100644 --- a/music_assistant/server/providers/ugp/__init__.py +++ b/music_assistant/server/providers/ugp/__init__.py @@ -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).""" diff --git a/music_assistant/server/providers/url/__init__.py b/music_assistant/server/providers/url/__init__.py index 38422d77..f9aeddb9 100644 --- a/music_assistant/server/providers/url/__init__.py +++ b/music_assistant/server/providers/url/__init__.py @@ -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: diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py index 9f2508a9..c0f243ab 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/server/providers/ytmusic/__init__.py @@ -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): diff --git a/music_assistant/server/server.py b/music_assistant/server/server.py index 8d3b648c..137b179e 100644 --- a/music_assistant/server/server.py +++ b/music_assistant/server/server.py @@ -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