Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playback using access token #397

Merged
merged 3 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 10 additions & 18 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ Status :warning:
=================

Spotify have recently disabled username and password login for playback
(`#394 <https://github.com/mopidy/mopidy-spotify/issues/394>`_).
Alternate authentication methods are possible but not yet supported.
(`#394 <https://github.com/mopidy/mopidy-spotify/issues/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
Expand All @@ -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)
Expand All @@ -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
<https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs>`_ to stream Spotify
audio, based on `librespot <https://github.com/librespot-org/librespot/>`_.
**This plugin is not yet available from apt.mopidy.com**. It must be either
Expand All @@ -83,6 +75,8 @@ Dependencies
or `Debian packages are available
<https://github.com/kingosticks/gst-plugins-rs-build/releases/latest>`_
for some platforms.
**We currently require a forked version of ``gst-plugins-spotify`` which supports
token-based login. This can be found `here <https://gitlab.freedesktop.org/kingosticks/gst-plugins-rs/-/tree/spotify-access-token>`_.

Verify the GStreamer spotify plugin is correctly installed::

Expand All @@ -106,8 +100,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 ...

Expand All @@ -116,9 +108,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.

Expand Down
4 changes: 2 additions & 2 deletions src/mopidy_spotify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/mopidy_spotify/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 0 additions & 2 deletions src/mopidy_spotify/ext.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
[spotify]
enabled = true
username =
password =
client_id =
client_secret =
bitrate = 160
Expand Down
15 changes: 14 additions & 1 deletion src/mopidy_spotify/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
self._auth = (client_id, client_secret)
else:
self._auth = None
self._access_token = None

self._base_url = base_url
self._refresh_url = refresh_url
Expand All @@ -69,6 +70,17 @@
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()
except OAuthTokenRefreshError as e:
logger.error(e) # noqa: TRY400
return None

Check warning on line 80 in src/mopidy_spotify/web.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_spotify/web.py#L74-L80

Added lines #L74 - L80 were not covered by tests
else:
return self._access_token

Check warning on line 82 in src/mopidy_spotify/web.py

View check run for this annotation

Codecov / codecov/patch

src/mopidy_spotify/web.py#L82

Added line #L82 was not covered by tests

def get(self, path, cache=None, *args, **kwargs):
if self._authorization_failed:
logger.debug("Blocking request as previous authorization failed.")
Expand Down Expand Up @@ -149,7 +161,8 @@
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"):
Expand Down
2 changes: 0 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ def config(tmp_path):
},
"proxy": {},
"spotify": {
"username": "alice",
"password": "password",
"bitrate": 160,
"volume_normalization": True,
"timeout": 10,
Expand Down
9 changes: 4 additions & 5 deletions tests/test_playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from mopidy import audio
from mopidy import backend as backend_api

from mopidy_spotify import backend
from mopidy_spotify import backend, web


@pytest.fixture
Expand All @@ -16,6 +16,7 @@ def audio_mock():
def backend_mock(config):
backend_mock = mock.Mock(spec=backend.SpotifyBackend)
backend_mock._config = config
backend_mock._web_client = mock.Mock(spec=web.OAuthClient)
return backend_mock


Expand All @@ -36,10 +37,9 @@ 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("bitrate", "160"),
mock.call("cache-credentials", cred_dir),
mock.call("access-token", mock.ANY),
mock.call("cache-files", spotify_cache_dir),
mock.call("cache-max-size", 8589934592),
]
Expand All @@ -53,10 +53,9 @@ 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("bitrate", "160"),
mock.call("cache-credentials", cred_dir),
mock.call("access-token", mock.ANY),
]


Expand Down