/
/
/
1"""Spotify music provider support for Music Assistant."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7from music_assistant_models.enums import ProviderFeature
8from music_assistant_models.errors import LoginFailed
9
10from music_assistant.constants import CONF_PROVIDERS
11
12from .constants import (
13 CONF_CLIENT_ID,
14 CONF_REFRESH_TOKEN_DEPRECATED,
15 CONF_REFRESH_TOKEN_DEV,
16 CONF_REFRESH_TOKEN_GLOBAL,
17)
18from .provider import SpotifyProvider
19
20if TYPE_CHECKING:
21 from music_assistant_models.config_entries import ProviderConfig
22 from music_assistant_models.provider import ProviderManifest
23
24 from music_assistant import MusicAssistant
25 from music_assistant.models import ProviderInstanceType
26
27SUPPORTED_FEATURES = {
28 ProviderFeature.LIBRARY_ARTISTS,
29 ProviderFeature.LIBRARY_ALBUMS,
30 ProviderFeature.LIBRARY_TRACKS,
31 ProviderFeature.LIBRARY_PLAYLISTS,
32 ProviderFeature.LIBRARY_ARTISTS_EDIT,
33 ProviderFeature.LIBRARY_ALBUMS_EDIT,
34 ProviderFeature.LIBRARY_PLAYLISTS_EDIT,
35 ProviderFeature.LIBRARY_TRACKS_EDIT,
36 ProviderFeature.PLAYLIST_CREATE,
37 ProviderFeature.PLAYLIST_TRACKS_EDIT,
38 ProviderFeature.BROWSE,
39 ProviderFeature.SEARCH,
40 ProviderFeature.ARTIST_ALBUMS,
41 ProviderFeature.ARTIST_TOPTRACKS,
42 ProviderFeature.SIMILAR_TRACKS,
43 ProviderFeature.LIBRARY_PODCASTS,
44 ProviderFeature.LIBRARY_PODCASTS_EDIT,
45}
46
47
48async def setup(
49 mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
50) -> ProviderInstanceType:
51 """Initialize provider(instance) with given configuration."""
52 _migrate_legacy_token(mass, config)
53 # a global refresh token lives in setup_data (new installs) or, for installs configured
54 # before setup flows existed, in the legacy config values (read-through covers both)
55 if not (
56 config.setup_data.get(CONF_REFRESH_TOKEN_GLOBAL)
57 or config.get_value(CONF_REFRESH_TOKEN_GLOBAL)
58 ):
59 raise LoginFailed("Re-Authentication required")
60 return SpotifyProvider(mass, manifest, config, SUPPORTED_FEATURES)
61
62
63def _migrate_legacy_token(mass: MusicAssistant, config: ProviderConfig) -> None:
64 """Move a legacy (pre global/dev split) refresh_token into setup_data, one-off."""
65 legacy_token = config.get_value(CONF_REFRESH_TOKEN_DEPRECATED)
66 if not legacy_token:
67 return
68 if config.setup_data.get(CONF_REFRESH_TOKEN_GLOBAL) or config.get_value(
69 CONF_REFRESH_TOKEN_GLOBAL
70 ):
71 return
72 # a custom client id means the legacy token authenticated a developer session.
73 # the setup_data read yields the value as stored (encrypted), which is only ever
74 # tested for presence and fed back to encrypt_string, which leaves an already
75 # encrypted value alone.
76 custom_client_id = config.setup_data.get(CONF_CLIENT_ID) or config.get_value(CONF_CLIENT_ID)
77 target_key = CONF_REFRESH_TOKEN_DEV if custom_client_id else CONF_REFRESH_TOKEN_GLOBAL
78 base = f"{CONF_PROVIDERS}/{config.instance_id}/setup_data"
79 encrypted = mass.config.encrypt_string(str(legacy_token))
80 mass.config.set(f"{base}/{target_key}", encrypted)
81 config.setup_data[target_key] = encrypted
82 if target_key == CONF_REFRESH_TOKEN_DEV and custom_client_id:
83 client_id_encrypted = mass.config.encrypt_string(str(custom_client_id))
84 mass.config.set(f"{base}/{CONF_CLIENT_ID}", client_id_encrypted)
85 config.setup_data[CONF_CLIENT_ID] = client_id_encrypted
86 # drop the deprecated legacy token now that it lives under its new key
87 mass.config.set_raw_provider_config_value(
88 config.instance_id, CONF_REFRESH_TOKEN_DEPRECATED, None
89 )
90