diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index 82879c0a9..f6364619c 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -27,7 +27,11 @@ ProviderConfig, ) from music_assistant.common.models.enums import EventType, PlayerState, ProviderType -from music_assistant.common.models.errors import InvalidDataError, PlayerUnavailableError +from music_assistant.common.models.errors import ( + InvalidDataError, + PlayerUnavailableError, + ProviderUnavailableError, +) from music_assistant.constants import ( CONF_CORE, CONF_PLAYERS, @@ -300,16 +304,6 @@ async def remove_provider_config_value(self, instance_id: str, key: str) -> None return self.remove(conf_key) - async def set_provider_config_value( - self, instance_id: str, key: str, value: ConfigValueType - ) -> None: - """Set single ProviderConfig value.""" - config = await self.get_provider_config(instance_id) - config.update({key: value}) - config.validate() - conf_key = f"{CONF_PROVIDERS}/{config.instance_id}" - self.set(conf_key, config.to_raw()) - @api_command("config/players") async def get_player_configs( self, provider: str | None = None, include_values: bool = False @@ -609,7 +603,7 @@ def get_raw_provider_config_value( ) def set_raw_provider_config_value( - self, provider_instance: str, key: str, value: ConfigValueType + self, provider_instance: str, key: str, value: ConfigValueType, encrypted: bool = False ) -> None: """ Set (raw) single config(entry) value for a provider. @@ -620,6 +614,12 @@ def set_raw_provider_config_value( # only allow setting raw values if main entry exists msg = f"Invalid provider_instance: {provider_instance}" raise KeyError(msg) + if encrypted: + value = self.encrypt_string(value) + # also update the cached value in the provider itself + if not (prov := self.mass.get_provider(provider_instance, return_unavailable=True)): + raise ProviderUnavailableError(provider_instance) + prov.config.values[key].value = value self.set(f"{CONF_PROVIDERS}/{provider_instance}/values/{key}", value) def set_raw_core_config_value(self, core_module: str, key: str, value: ConfigValueType) -> None: diff --git a/music_assistant/server/models/provider.py b/music_assistant/server/models/provider.py index 95148cdc3..f7557da2e 100644 --- a/music_assistant/server/models/provider.py +++ b/music_assistant/server/models/provider.py @@ -50,6 +50,9 @@ def lookup_key(self) -> str: # should not be overridden in normal circumstances return self.instance_id if self.manifest.multi_instance else self.domain + async def handle_async_init(self) -> None: + """Handle async initialization of the provider.""" + async def loaded_in_mass(self) -> None: """Call after the provider has been loaded.""" diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index b1af1eb25..7b5f80ae8 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -131,9 +131,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = AirplayProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return AirplayProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/apple_music/__init__.py b/music_assistant/server/providers/apple_music/__init__.py index 12aae1e9c..e25ea8358 100644 --- a/music_assistant/server/providers/apple_music/__init__.py +++ b/music_assistant/server/providers/apple_music/__init__.py @@ -76,9 +76,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = AppleMusicProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return AppleMusicProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/builtin/__init__.py b/music_assistant/server/providers/builtin/__init__.py index f609e56c3..0d8b9a178 100644 --- a/music_assistant/server/providers/builtin/__init__.py +++ b/music_assistant/server/providers/builtin/__init__.py @@ -337,7 +337,7 @@ async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool if media_type == MediaType.PLAYLIST and prov_item_id in BUILTIN_PLAYLISTS: # user wants to disable/remove one of our builtin playlists # to prevent it comes back, we mark it as disabled in config - await self.mass.config.set_provider_config_value(self.instance_id, prov_item_id, False) + self.mass.config.set_raw_provider_config_value(self.instance_id, prov_item_id, False) return True if media_type == MediaType.TRACK: # regular manual track URL/path diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/server/providers/deezer/__init__.py index 7423ed00a..10294087d 100644 --- a/music_assistant/server/providers/deezer/__init__.py +++ b/music_assistant/server/providers/deezer/__init__.py @@ -123,9 +123,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = DeezerProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return DeezerProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index aa9be58ce..c5de303fd 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -94,9 +94,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = DLNAPlayerProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return DLNAPlayerProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/fanarttv/__init__.py b/music_assistant/server/providers/fanarttv/__init__.py index 100c0c9fe..d3bac5750 100644 --- a/music_assistant/server/providers/fanarttv/__init__.py +++ b/music_assistant/server/providers/fanarttv/__init__.py @@ -43,9 +43,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = FanartTvMetadataProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return FanartTvMetadataProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/filesystem_smb/__init__.py b/music_assistant/server/providers/filesystem_smb/__init__.py index e0b5f7307..fd47cc67e 100644 --- a/music_assistant/server/providers/filesystem_smb/__init__.py +++ b/music_assistant/server/providers/filesystem_smb/__init__.py @@ -49,10 +49,7 @@ async def setup( if not share or "/" in share or "\\" in share: msg = "Invalid share name" raise LoginFailed(msg) - prov = SMBFileSystemProvider(mass, manifest, config) - await prov.handle_async_init() - await prov.check_write_access() - return prov + return SMBFileSystemProvider(mass, manifest, config) async def get_config_entries( @@ -152,6 +149,8 @@ async def handle_async_init(self) -> None: msg = f"Connection failed for the given details: {err}" raise LoginFailed(msg) from err + await self.check_write_access() + async def unload(self) -> None: """ Handle unload/close of the provider. diff --git a/music_assistant/server/providers/fully_kiosk/__init__.py b/music_assistant/server/providers/fully_kiosk/__init__.py index 28e4a1cc8..32d7ec0c5 100644 --- a/music_assistant/server/providers/fully_kiosk/__init__.py +++ b/music_assistant/server/providers/fully_kiosk/__init__.py @@ -47,9 +47,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = FullyKioskProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return FullyKioskProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/hass/__init__.py b/music_assistant/server/providers/hass/__init__.py index d704fe9f0..4966c8365 100644 --- a/music_assistant/server/providers/hass/__init__.py +++ b/music_assistant/server/providers/hass/__init__.py @@ -49,9 +49,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = HomeAssistant(mass, manifest, config) - await prov.handle_async_init() - return prov + return HomeAssistant(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/jellyfin/__init__.py b/music_assistant/server/providers/jellyfin/__init__.py index f2699e329..b0d9c9c6e 100644 --- a/music_assistant/server/providers/jellyfin/__init__.py +++ b/music_assistant/server/providers/jellyfin/__init__.py @@ -76,9 +76,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = JellyfinProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return JellyfinProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/musicbrainz/__init__.py b/music_assistant/server/providers/musicbrainz/__init__.py index 2a2b7e9ca..1d6ea349e 100644 --- a/music_assistant/server/providers/musicbrainz/__init__.py +++ b/music_assistant/server/providers/musicbrainz/__init__.py @@ -49,9 +49,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = MusicbrainzProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return MusicbrainzProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py index 29f684969..75930d838 100644 --- a/music_assistant/server/providers/plex/__init__.py +++ b/music_assistant/server/providers/plex/__init__.py @@ -99,9 +99,7 @@ async def setup( msg = "Invalid login credentials" raise LoginFailed(msg) - prov = PlexProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return PlexProvider(mass, manifest, config) async def get_config_entries( # noqa: PLR0915 diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/server/providers/qobuz/__init__.py index 786173af4..c745e50df 100644 --- a/music_assistant/server/providers/qobuz/__init__.py +++ b/music_assistant/server/providers/qobuz/__init__.py @@ -87,9 +87,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = QobuzProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return QobuzProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/radiobrowser/__init__.py b/music_assistant/server/providers/radiobrowser/__init__.py index 80bdae87f..6ecc01418 100644 --- a/music_assistant/server/providers/radiobrowser/__init__.py +++ b/music_assistant/server/providers/radiobrowser/__init__.py @@ -56,10 +56,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = RadioBrowserProvider(mass, manifest, config) - - await prov.handle_async_init() - return prov + return RadioBrowserProvider(mass, manifest, config) async def get_config_entries( @@ -237,7 +234,7 @@ async def library_add(self, item: MediaItemType) -> bool: return False self.logger.debug("Adding radio %s to stored radios", item.item_id) stored_radios = [*stored_radios, item.item_id] - await self.mass.config.set_provider_config_value( + self.mass.config.set_raw_provider_config_value( self.instance_id, CONF_STORED_RADIOS, stored_radios ) return True @@ -251,7 +248,7 @@ async def library_remove(self, prov_item_id: str, media_type: MediaType) -> bool return False self.logger.debug("Removing radio %s from stored radios", prov_item_id) stored_radios = [x for x in stored_radios if x != prov_item_id] - await self.mass.config.set_provider_config_value( + self.mass.config.set_raw_provider_config_value( self.instance_id, CONF_STORED_RADIOS, stored_radios ) return True diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index 2f46538dd..1280ead7e 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -141,9 +141,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = SlimprotoProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return SlimprotoProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/snapcast/__init__.py b/music_assistant/server/providers/snapcast/__init__.py index 131f4348c..18284877c 100644 --- a/music_assistant/server/providers/snapcast/__init__.py +++ b/music_assistant/server/providers/snapcast/__init__.py @@ -84,9 +84,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = SnapCastProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return SnapCastProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index cca3fc861..9dd76072e 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -100,7 +100,6 @@ async def setup( logging.getLogger("soco").setLevel(logging.DEBUG) else: logging.getLogger("soco").setLevel(prov.logger.level + 10) - await prov.handle_async_init() return prov diff --git a/music_assistant/server/providers/soundcloud/__init__.py b/music_assistant/server/providers/soundcloud/__init__.py index a4e091adf..b730fbe2f 100644 --- a/music_assistant/server/providers/soundcloud/__init__.py +++ b/music_assistant/server/providers/soundcloud/__init__.py @@ -57,9 +57,7 @@ async def setup( if not config.get_value(CONF_CLIENT_ID) or not config.get_value(CONF_AUTHORIZATION): msg = "Invalid login credentials" raise LoginFailed(msg) - prov = SoundcloudMusicProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return SoundcloudMusicProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/server/providers/spotify/__init__.py index ec8154550..342bb7503 100644 --- a/music_assistant/server/providers/spotify/__init__.py +++ b/music_assistant/server/providers/spotify/__init__.py @@ -22,6 +22,7 @@ LoginFailed, MediaNotFoundError, ResourceTemporarilyUnavailable, + SetupFailedError, ) from music_assistant.common.models.media_items import ( Album, @@ -61,9 +62,7 @@ CONF_CLIENT_ID = "client_id" CONF_ACTION_AUTH = "auth" -CONF_ACCESS_TOKEN = "access_token" CONF_REFRESH_TOKEN = "refresh_token" -CONF_AUTH_EXPIRES_AT = "expires_at" CONF_ACTION_CLEAR_AUTH = "clear_auth" SCOPE = [ "playlist-read", @@ -115,9 +114,10 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = SpotifyProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + if not config.get_value(CONF_REFRESH_TOKEN): + msg = "Re-Authentication required" + raise SetupFailedError(msg) + return SpotifyProvider(mass, manifest, config) async def get_config_entries( @@ -167,15 +167,12 @@ async def get_config_entries( "https://accounts.spotify.com/api/token", data=params ) as response: result = await response.json() - values[CONF_ACCESS_TOKEN] = result["access_token"] values[CONF_REFRESH_TOKEN] = result["refresh_token"] - values[CONF_AUTH_EXPIRES_AT] = int(time.time() + result["expires_in"]) - auth_required = values.get(CONF_REFRESH_TOKEN) is None + auth_required = values.get(CONF_REFRESH_TOKEN) in (None, "") if auth_required: values[CONF_CLIENT_ID] = None - values[CONF_ACCESS_TOKEN] = None label_text = ( "You need to authenticate to Spotify. Click the authenticate button below " "to start the authentication process which will open in a new (popup) window, " @@ -192,13 +189,6 @@ async def get_config_entries( type=ConfigEntryType.LABEL, label=label_text, ), - ConfigEntry( - key=CONF_ACCESS_TOKEN, - type=ConfigEntryType.SECURE_STRING, - label=CONF_ACCESS_TOKEN, - hidden=True, - value=values.get(CONF_ACCESS_TOKEN) if values else None, - ), ConfigEntry( key=CONF_REFRESH_TOKEN, type=ConfigEntryType.SECURE_STRING, @@ -207,14 +197,6 @@ async def get_config_entries( required=True, value=values.get(CONF_REFRESH_TOKEN) if values else None, ), - ConfigEntry( - key=CONF_AUTH_EXPIRES_AT, - type=ConfigEntryType.INTEGER, - label=CONF_AUTH_EXPIRES_AT, - hidden=True, - default_value=0, - value=values.get(CONF_AUTH_EXPIRES_AT) if values else None, - ), ConfigEntry( key=CONF_CLIENT_ID, type=ConfigEntryType.SECURE_STRING, @@ -259,6 +241,9 @@ async def handle_async_init(self) -> None: # try login which will raise if it fails await self.login() + async def loaded_in_mass(self) -> None: + """Call after the provider has been loaded.""" + @property def supported_features(self) -> tuple[ProviderFeature, ...]: """Return the features supported by this Provider.""" @@ -541,6 +526,8 @@ async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]: async def get_stream_details(self, item_id: str) -> StreamDetails: """Return the content details for the given track when it will be streamed.""" + # make sure that the token is still valid by just requesting it + await self.login() return StreamDetails( item_id=item_id, provider=self.instance_id, @@ -554,7 +541,6 @@ async def get_audio_stream( self, streamdetails: StreamDetails, seek_position: int = 0 ) -> AsyncGenerator[bytes, None]: """Return the audio stream for the provider item.""" - # make sure that the token is still valid by just requesting it auth_info = await self.login() librespot = await self.get_librespot_binary() args = [ @@ -772,54 +758,36 @@ async def login(self) -> dict: if self._auth_info and (self._auth_info["expires_at"] > (time.time() - 120)): return self._auth_info + # request new access token using the refresh token if not (refresh_token := self.config.get_value(CONF_REFRESH_TOKEN)): raise LoginFailed("Authentication required") - expires_at = self.config.get_value(CONF_AUTH_EXPIRES_AT) or 0 - access_token = self.config.get_value(CONF_ACCESS_TOKEN) - - if expires_at < (time.time() - 300): - # refresh token - client_id = self.config.get_value(CONF_CLIENT_ID) or app_var(2) - params = { - "grant_type": "refresh_token", - "refresh_token": refresh_token, - "client_id": client_id, - } - async with self.mass.http_session.post( - "https://accounts.spotify.com/api/token", data=params - ) as response: - if response.status != 200: - err = await response.text() - if "revoked" in err: - # clear refresh token if it's invalid - self.mass.config.set_raw_provider_config_value( - self.instance_id, CONF_REFRESH_TOKEN, None - ) - raise LoginFailed(f"Failed to refresh access token: {err}") - data = await response.json() - access_token = data.get("access_token") or access_token - refresh_token = data.get("refresh_token") or refresh_token - expires_at = int(data["expires_in"] + time.time()) - self.logger.debug("Successfully refreshed access token") - - self._auth_info = auth_info = { - "access_token": access_token, + client_id = self.config.get_value(CONF_CLIENT_ID) or app_var(2) + params = { + "grant_type": "refresh_token", "refresh_token": refresh_token, - "expires_at": expires_at, + "client_id": client_id, } - - # make sure that our updated creds get stored in config - await self.mass.config.set_provider_config_value( - self.instance_id, CONF_REFRESH_TOKEN, refresh_token - ) - await self.mass.config.set_provider_config_value( - self.instance_id, CONF_ACCESS_TOKEN, access_token - ) - await self.mass.config.set_provider_config_value( - self.instance_id, CONF_AUTH_EXPIRES_AT, expires_at + async with self.mass.http_session.post( + "https://accounts.spotify.com/api/token", data=params + ) as response: + if response.status != 200: + err = await response.text() + if "revoked" in err: + # clear refresh token if it's invalid + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_REFRESH_TOKEN, "" + ) + raise LoginFailed(f"Failed to refresh access token: {err}") + auth_info = await response.json() + auth_info["expires_at"] = int(auth_info["expires_in"] + time.time()) + self.logger.debug("Successfully refreshed access token") + + # make sure that our updated creds get stored in memory + config config + self._auth_info = auth_info + self.mass.config.set_raw_provider_config_value( + self.instance_id, CONF_REFRESH_TOKEN, auth_info["refresh_token"], encrypted=True ) - # get logged-in user info if not self._sp_user: self._sp_user = userinfo = await self._get_data("me", auth_info=auth_info) @@ -871,6 +839,11 @@ async def _get_data(self, endpoint, **kwargs) -> dict[str, Any]: if response.status in (502, 503): raise ResourceTemporarilyUnavailable(backoff_time=30) + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=1) + # handle 404 not found, convert to MediaNotFoundError if response.status == 404: raise MediaNotFoundError(f"{endpoint} not found") @@ -912,6 +885,11 @@ async def _put_data(self, endpoint, data=None, **kwargs) -> None: raise ResourceTemporarilyUnavailable( "Spotify Rate Limiter", backoff_time=backoff_time ) + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=1) + # handle temporary server error if response.status in (502, 503): raise ResourceTemporarilyUnavailable(backoff_time=30) @@ -932,6 +910,10 @@ async def _post_data(self, endpoint, data=None, **kwargs) -> dict[str, Any]: raise ResourceTemporarilyUnavailable( "Spotify Rate Limiter", backoff_time=backoff_time ) + # handle token expired, raise ResourceTemporarilyUnavailable + # so it will be retried (and the token refreshed) + if response.status == 401: + raise ResourceTemporarilyUnavailable("Token expired", backoff_time=1) # handle temporary server error if response.status in (502, 503): raise ResourceTemporarilyUnavailable(backoff_time=30) diff --git a/music_assistant/server/providers/theaudiodb/__init__.py b/music_assistant/server/providers/theaudiodb/__init__.py index 578069d1d..2136d3545 100644 --- a/music_assistant/server/providers/theaudiodb/__init__.py +++ b/music_assistant/server/providers/theaudiodb/__init__.py @@ -82,9 +82,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = AudioDbMetadataProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return AudioDbMetadataProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/providers/tidal/__init__.py b/music_assistant/server/providers/tidal/__init__.py index ab49f7eca..46bed566b 100644 --- a/music_assistant/server/providers/tidal/__init__.py +++ b/music_assistant/server/providers/tidal/__init__.py @@ -124,9 +124,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = TidalProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return TidalProvider(mass, manifest, config) async def tidal_auth_url(auth_helper: AuthenticationHelper, quality: str) -> str: @@ -349,7 +347,7 @@ async def handle_async_init(self) -> None: self.mass.config.set_raw_provider_config_value( self.instance_id, CONF_REFRESH_TOKEN, None ) - raise LoginFailed("Credentials, expired, you need to re-setup") + raise LoginFailed("Credentials expired, you need to re-setup") raise @property @@ -644,17 +642,19 @@ async def _get_tidal_session(self) -> TidalSession: refresh_token=str(self.config.get_value(CONF_REFRESH_TOKEN)), expiry_time=datetime.fromisoformat(str(self.config.get_value(CONF_EXPIRY_TIME))), ) - await self.mass.config.set_provider_config_value( + self.mass.config.set_raw_provider_config_value( self.config.instance_id, CONF_AUTH_TOKEN, self._tidal_session.access_token, + encrypted=True, ) - await self.mass.config.set_provider_config_value( + self.mass.config.set_raw_provider_config_value( self.config.instance_id, CONF_REFRESH_TOKEN, self._tidal_session.refresh_token, + encrypted=True, ) - await self.mass.config.set_provider_config_value( + self.mass.config.set_raw_provider_config_value( self.config.instance_id, CONF_EXPIRY_TIME, self._tidal_session.expiry_time.isoformat(), diff --git a/music_assistant/server/providers/tunein/__init__.py b/music_assistant/server/providers/tunein/__init__.py index 555fa8993..50f46068a 100644 --- a/music_assistant/server/providers/tunein/__init__.py +++ b/music_assistant/server/providers/tunein/__init__.py @@ -43,14 +43,7 @@ async def setup( msg = "Username is invalid" raise LoginFailed(msg) - prov = TuneInProvider(mass, manifest, config) - if "@" in config.get_value(CONF_USERNAME): - prov.logger.warning( - "Email address detected instead of username, " - "it is advised to use the tunein username instead of email." - ) - await prov.handle_async_init() - return prov + return TuneInProvider(mass, manifest, config) async def get_config_entries( @@ -90,6 +83,11 @@ def supported_features(self) -> tuple[ProviderFeature, ...]: async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" self._throttler = Throttler(rate_limit=1, period=2) + if "@" in self.config.get_value(CONF_USERNAME): + self.logger.warning( + "Email address detected instead of username, " + "it is advised to use the tunein username instead of email." + ) async def get_library_radios(self) -> AsyncGenerator[Radio, None]: """Retrieve library/subscribed radio stations from the provider.""" diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py index eccbef138..030d37bb0 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/server/providers/ytmusic/__init__.py @@ -118,9 +118,7 @@ async def setup( mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig ) -> ProviderInstanceType: """Initialize provider(instance) with given configuration.""" - prov = YoutubeMusicProvider(mass, manifest, config) - await prov.handle_async_init() - return prov + return YoutubeMusicProvider(mass, manifest, config) async def get_config_entries( diff --git a/music_assistant/server/server.py b/music_assistant/server/server.py index 7799337fc..dc570a84e 100644 --- a/music_assistant/server/server.py +++ b/music_assistant/server/server.py @@ -606,6 +606,11 @@ async def _load_provider(self, conf: ProviderConfig) -> None: except TimeoutError as err: msg = f"Provider {domain} did not load within 30 seconds" raise SetupFailedError(msg) from err + + self._providers[provider.instance_id] = provider + # run async setup + await provider.handle_async_init() + # if we reach this point, the provider loaded successfully LOGGER.info( "Loaded %s provider %s", @@ -613,7 +618,7 @@ async def _load_provider(self, conf: ProviderConfig) -> None: conf.name or conf.domain, ) 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())