Fix Spotify token gets invalidated every hour (#1575)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 18 Aug 2024 16:47:08 +0000 (18:47 +0200)
committerGitHub <noreply@github.com>
Sun, 18 Aug 2024 16:47:08 +0000 (18:47 +0200)
26 files changed:
music_assistant/server/controllers/config.py
music_assistant/server/models/provider.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/apple_music/__init__.py
music_assistant/server/providers/builtin/__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_smb/__init__.py
music_assistant/server/providers/fully_kiosk/__init__.py
music_assistant/server/providers/hass/__init__.py
music_assistant/server/providers/jellyfin/__init__.py
music_assistant/server/providers/musicbrainz/__init__.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/ytmusic/__init__.py
music_assistant/server/server.py

index 82879c0a99d1a31dd9579ef2f41788af9c0c1f94..f6364619cb53ab9517be684922848bb7be01bd8a 100644 (file)
@@ -27,7 +27,11 @@ from music_assistant.common.models.config_entries import (
     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 @@ class ConfigController:
             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 @@ class ConfigController:
         )
 
     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 @@ class ConfigController:
             # 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:
index 95148cdc375e3046e0fb615be10c1e9ae7b81842..f7557da2edc1365c8c0c3d53b06802475e9f7bfe 100644 (file)
@@ -50,6 +50,9 @@ class Provider:
         # 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."""
 
index b1af1eb254165bc1a4e7f4b55cc0cb42f963c51b..7b5f80ae8601373f64122b744ce2213289c33827 100644 (file)
@@ -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(
index 12aae1e9c8133ae8f8e4e55d1aa84444b3340a54..e25ea8358e8bd2c668838982eb84fafeb02e0381 100644 (file)
@@ -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(
index f609e56c39dfc95b504b51a7f693b39bc2e92916..0d8b9a178467b8f7acd8806d59d84cca3da483b5 100644 (file)
@@ -337,7 +337,7 @@ class BuiltinProvider(MusicProvider):
         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
index 7423ed00ad82ec000b0ec529e9adfb538f790a4b..10294087d77b9f87f00c2df0c54331f7ebdf6a31 100644 (file)
@@ -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(
index aa9be58ceac3d14cdac975e2563c598e5f4d2811..c5de303fdb5b01579e11b62fe75c225c2a738d6e 100644 (file)
@@ -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(
index 100c0c9feccf37f0c0c354b6e48a444fb8db2388..d3bac5750c16e9eb9d04e7137a54a02eb1eb21db 100644 (file)
@@ -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(
index e0b5f73072a6047be1547cb7cb80e4b7c0c13ed4..fd47cc67ea45604bf7abce2be0e368ff6a5c69fb 100644 (file)
@@ -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 @@ class SMBFileSystemProvider(LocalFileSystemProvider):
             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.
index 28e4a1cc8d733e83dc99752d1aaa8fdcc79f36c1..32d7ec0c5a937eca867da231005497e509fab65c 100644 (file)
@@ -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(
index d704fe9f0511f1bc3be671c94709f46864d92b68..4966c8365f5607040718d5428d68835dd54fa57e 100644 (file)
@@ -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(
index f2699e329bfabd795c4e6c529d0ccd31d95b84b2..b0d9c9c6e3ec899f03aaeff1c8f1766d74ebd895 100644 (file)
@@ -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(
index 2a2b7e9ca5d827c99e24eeb7bc7d1c6d58f12925..1d6ea349e4955cc18c499ec88027a0bfa1d52948 100644 (file)
@@ -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(
index 29f68496942fb7a8e906045f000bbc798d2c9d2f..75930d838337e932e6edf582c4ab30e3cf5c53ea 100644 (file)
@@ -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
index 786173af46806b2de4c4e5fabbe3bed676e26d07..c745e50dfa79438ffeecda9873fc8f0f6b6db0e9 100644 (file)
@@ -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(
index 80bdae87f0bf473358ed63980bf1c4195916ece1..6ecc014188c40e6039bf64f5e1842bc00fa2883c 100644 (file)
@@ -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 @@ class RadioBrowserProvider(MusicProvider):
             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 @@ class RadioBrowserProvider(MusicProvider):
             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
index 2f46538dd0c624b19623ea180d60744f2a323652..1280ead7e05307ab808f223ff01963329f772c91 100644 (file)
@@ -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(
index 131f4348cdd84c317f752ce5bcd10e3f4a4e1315..18284877c7eb25db1d957f5bcc4b267f17d07940 100644 (file)
@@ -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(
index cca3fc861d7bebf82335ddff3f1e354261ceead1..9dd76072e88334e2b678873d755f5aefd264fdd6 100644 (file)
@@ -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
 
 
index a4e091adfdad968b94623ff41671396f5158a152..b730fbe2f07984cbee36b3fc7cf606273bbde600 100644 (file)
@@ -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(
index ec81545504ad8862f15716c1261d9982bfd0d752..342bb75038c7b1168eb17208ab7a43b6043743b3 100644 (file)
@@ -22,6 +22,7 @@ from music_assistant.common.models.errors import (
     LoginFailed,
     MediaNotFoundError,
     ResourceTemporarilyUnavailable,
+    SetupFailedError,
 )
 from music_assistant.common.models.media_items import (
     Album,
@@ -61,9 +62,7 @@ if TYPE_CHECKING:
 
 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 @@ class SpotifyProvider(MusicProvider):
         # 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 @@ class SpotifyProvider(MusicProvider):
 
     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 @@ class SpotifyProvider(MusicProvider):
         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 @@ class SpotifyProvider(MusicProvider):
         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 @@ class SpotifyProvider(MusicProvider):
             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 @@ class SpotifyProvider(MusicProvider):
                 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 @@ class SpotifyProvider(MusicProvider):
                 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)
index 578069d1d84c1c4e9f15de51d6f74d568ab9b62a..2136d35452873e6a6fccefdc0bbc004393bd9772 100644 (file)
@@ -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(
index ab49f7eca9bda487be6caab8c2e91dc3ff8a8876..46bed566bf661d56553624e8ea75ee206b69a1e0 100644 (file)
@@ -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 @@ class TidalProvider(MusicProvider):
                 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 @@ class TidalProvider(MusicProvider):
             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(),
index 555fa899318caf1df730ad9c640ab89f8bc8e339..50f46068ab54cf584baf02c04029b0ad2f87874d 100644 (file)
@@ -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 @@ class TuneInProvider(MusicProvider):
     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."""
index eccbef1382d0cccbfc93a446becc3f0970870d7f..030d37bb092ab4619f1d84c12b8c1dff27a48c8e 100644 (file)
@@ -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(
index 7799337fce808937fc216d1f87be8470084329dc..dc570a84eb5f5deec009198a101e7c05a952b492 100644 (file)
@@ -606,6 +606,11 @@ class MusicAssistant:
         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 @@ class MusicAssistant:
             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())