diff --git a/README.rst b/README.rst index 6379ed67..c6d0bc37 100644 --- a/README.rst +++ b/README.rst @@ -22,13 +22,12 @@ Status :warning: ================= Spotify have recently disabled username and password login for playback -(`#394 `_). -Alternate authentication methods are possible but not yet supported. +(`#394 `_) and we +now utilise access-token login. You no longer need to provide your +Spotify account username or password. Mopidy-Spotify currently has no support for the following: -- Playback - - Seeking - Gapless playback @@ -48,6 +47,8 @@ Mopidy-Spotify currently has no support for the following: Working support for the following features is currently available: +- Playback + - Search - Playlists (read-only) @@ -63,18 +64,9 @@ Dependencies - A Spotify Premium subscription. Mopidy-Spotify **will not** work with Spotify Free, just Spotify Premium. -- A non-Facebook Spotify username and password. If you created your account - through Facebook you'll need to create a "device password" to be able to use - Mopidy-Spotify. Go to http://www.spotify.com/account/set-device-password/, - login with your Facebook account, and follow the instructions. However, - sometimes that process can fail for users with Facebook logins, in which case - you can create an app-specific password on Facebook by going to facebook.com > - Settings > Security > App passwords > Generate app passwords, and generate one - to use with Mopidy-Spotify. - - ``Mopidy`` >= 3.4. The music server that Mopidy-Spotify extends. -- ``gst-plugins-spotify`` >= 0.10. The `GStreamer Rust Plugin +- A *custom* version of ``gst-plugins-spotify``. The `GStreamer Rust Plugin `_ to stream Spotify audio, based on `librespot `_. **This plugin is not yet available from apt.mopidy.com**. It must be either @@ -83,6 +75,9 @@ Dependencies or `Debian packages are available `_ for some platforms. + **We currently require a forked version of ``gst-plugins-spotify`` which supports + token-based login. This can be found `here + `_. Verify the GStreamer spotify plugin is correctly installed:: @@ -106,8 +101,6 @@ https://mopidy.com/ext/spotify/#authentication to authorize this extension against your Spotify account:: [spotify] - username = alice - password = secret client_id = ... client_id value you got from mopidy.com ... client_secret = ... client_secret value you got from mopidy.com ... @@ -116,9 +109,9 @@ The following configuration values are available: - ``spotify/enabled``: If the Spotify extension should be enabled or not. Defaults to ``true``. -- ``spotify/username``: Your Spotify Premium username. You *must* provide this. +- ``spotify/username``: Your Spotify Premium username. Obsolete. -- ``spotify/password``: Your Spotify Premium password. You *must* provide this. +- ``spotify/password``: Your Spotify Premium password. Obsolete. - ``spotify/client_id``: Your Spotify application client id. You *must* provide this. diff --git a/src/mopidy_spotify/__init__.py b/src/mopidy_spotify/__init__.py index 5b3130c6..cfd174de 100644 --- a/src/mopidy_spotify/__init__.py +++ b/src/mopidy_spotify/__init__.py @@ -17,8 +17,8 @@ def get_default_config(self): def get_config_schema(self): schema = super().get_config_schema() - schema["username"] = config.String() - schema["password"] = config.Secret() + schema["username"] = config.Deprecated() # since 5.0 + schema["password"] = config.Deprecated() # since 5.0 schema["client_id"] = config.String() schema["client_secret"] = config.Secret() diff --git a/src/mopidy_spotify/backend.py b/src/mopidy_spotify/backend.py index 73c51795..5e25a3be 100644 --- a/src/mopidy_spotify/backend.py +++ b/src/mopidy_spotify/backend.py @@ -45,9 +45,9 @@ def __init__(self, *args, **kwargs): self._credentials_dir.mkdir(mode=0o700) def on_source_setup(self, source): - for prop in ["username", "password", "bitrate"]: - source.set_property(prop, str(self._config[prop])) + source.set_property("bitrate", str(self._config["bitrate"])) source.set_property("cache-credentials", self._credentials_dir) + source.set_property("access-token", self.backend._web_client.token()) if self._config["allow_cache"]: source.set_property("cache-files", self._cache_location) source.set_property("cache-max-size", self._config["cache_size"] * 1048576) diff --git a/src/mopidy_spotify/ext.conf b/src/mopidy_spotify/ext.conf index d66615a4..6e9cb450 100644 --- a/src/mopidy_spotify/ext.conf +++ b/src/mopidy_spotify/ext.conf @@ -1,7 +1,5 @@ [spotify] enabled = true -username = -password = client_id = client_secret = bitrate = 160 diff --git a/src/mopidy_spotify/web.py b/src/mopidy_spotify/web.py index d51888f4..c62b6a3d 100644 --- a/src/mopidy_spotify/web.py +++ b/src/mopidy_spotify/web.py @@ -50,6 +50,7 @@ def __init__( # noqa: PLR0913 self._auth = (client_id, client_secret) else: self._auth = None + self._access_token = None self._base_url = base_url self._refresh_url = refresh_url @@ -69,6 +70,17 @@ def __init__( # noqa: PLR0913 self._cache_mutex = threading.Lock() # Protects get() cache param. self._refresh_mutex = threading.Lock() # Protects _headers and _expires. + def token(self): + with self._refresh_mutex: + try: + if self._should_refresh_token(): + self._refresh_token() + logger.info(f"Providing access token: {self._access_token}") + return self._access_token + except OAuthTokenRefreshError as e: + logger.error(e) # noqa: TRY400 + return None + def get(self, path, cache=None, *args, **kwargs): if self._authorization_failed: logger.debug("Blocking request as previous authorization failed.") @@ -149,7 +161,8 @@ def _refresh_token(self): f"wrong token_type: {result.get('token_type')}" ) - self._headers["Authorization"] = f"Bearer {result['access_token']}" + self._access_token = result['access_token'] + self._headers["Authorization"] = f"Bearer {self._access_token}" self._expires = time.time() + result.get("expires_in", float("Inf")) if result.get("expires_in"): diff --git a/tests/conftest.py b/tests/conftest.py index a7708868..609f7b79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,8 +21,6 @@ def config(tmp_path): }, "proxy": {}, "spotify": { - "username": "alice", - "password": "password", "bitrate": 160, "volume_normalization": True, "timeout": 10, diff --git a/tests/test_playback.py b/tests/test_playback.py index 575e74e0..8d1367f8 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -35,8 +35,7 @@ def test_on_source_setup_sets_properties(config, provider): cred_dir = spotify_data_dir / "credentials-cache" assert mock_source.set_property.mock_calls == [ - mock.call("username", "alice"), - mock.call("password", "password"), + mock.call("access-token", mock.ANY), mock.call("bitrate", "160"), mock.call("cache-credentials", cred_dir), mock.call("cache-files", spotify_cache_dir), @@ -52,8 +51,7 @@ def test_on_source_setup_without_caching(config, provider): cred_dir = spotify_data_dir / "credentials-cache" assert mock_source.set_property.mock_calls == [ - mock.call("username", "alice"), - mock.call("password", "password"), + mock.call("access-token", mock.ANY), mock.call("bitrate", "160"), mock.call("cache-credentials", cred_dir), ]