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

Web API playlists v3 #235

Merged
merged 15 commits into from
Dec 12, 2019
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 0 additions & 1 deletion mopidy_spotify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import pathlib

import pkg_resources

from mopidy import config, ext

__version__ = pkg_resources.get_distribution("Mopidy-Spotify").version
Expand Down
35 changes: 10 additions & 25 deletions mopidy_spotify/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import threading

import pykka
import spotify

from mopidy import backend, httpclient

import spotify
from mopidy_spotify import Extension, library, playback, playlists, web

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,13 +57,15 @@ def on_start(self):
self._config["spotify"]["password"],
)

self._web_client = web.OAuthClient(
base_url="https://api.spotify.com/v1",
refresh_url="https://auth.mopidy.com/spotify/token",
client_id=self._config["spotify"]["client_id"],
client_secret=self._config["spotify"]["client_secret"],
proxy_config=self._config["proxy"],
self._web_client = web.SpotifyOAuthClient(
self._config["spotify"]["client_id"],
self._config["spotify"]["client_secret"],
self._config["proxy"],
)
self._web_client.login()

if self.playlists is not None:
self.playlists.refresh()

def on_stop(self):
logger.debug("Logging out of Spotify")
Expand Down Expand Up @@ -126,23 +128,6 @@ def on_logged_in(self):
logger.info("Spotify private session activated")
self._session.social.private_session = True

self._session.playlist_container.on(
spotify.PlaylistContainerEvent.CONTAINER_LOADED,
playlists.on_container_loaded,
)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_ADDED,
playlists.on_playlist_added,
)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_REMOVED,
playlists.on_playlist_removed,
)
self._session.playlist_container.on(
spotify.PlaylistContainerEvent.PLAYLIST_MOVED,
playlists.on_playlist_moved,
)

def on_play_token_lost(self):
if self._session.player.state == spotify.PlayerState.PLAYING:
self.playback.pause()
Expand Down
4 changes: 2 additions & 2 deletions mopidy_spotify/browse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import logging

import spotify

from mopidy import models

import spotify
from mopidy_spotify import countries, translator

logger = logging.getLogger(__name__)
Expand Down
1 change: 0 additions & 1 deletion mopidy_spotify/distinct.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging

import spotify

from mopidy_spotify import search

logger = logging.getLogger(__name__)
Expand Down
5 changes: 4 additions & 1 deletion mopidy_spotify/library.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from mopidy import backend

from mopidy_spotify import browse, distinct, images, lookup, search

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -29,7 +30,9 @@ def get_images(self, uris):
return images.get_images(self._backend._web_client, uris)

def lookup(self, uri):
return lookup.lookup(self._config, self._backend._session, uri)
return lookup.lookup(
self._config, self._backend._session, self._backend._web_client, uri
)

def search(self, query=None, uris=None, exact=False):
return search.search(
Expand Down
32 changes: 15 additions & 17 deletions mopidy_spotify/lookup.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import logging

import spotify

from mopidy_spotify import translator, utils
from mopidy_spotify import playlists, translator, utils, web

logger = logging.getLogger(__name__)

Expand All @@ -11,25 +10,25 @@
]


def lookup(config, session, uri):
def lookup(config, session, web_client, uri):
try:
sp_link = session.get_link(uri)
web_link = web.parse_uri(uri)
if web_link.type != "playlist":
sp_link = session.get_link(uri)
except ValueError as exc:
logger.info(f'Failed to lookup "{uri}": {exc}')
return []

try:
if sp_link.type is spotify.LinkType.TRACK:
if web_link.type == "playlist":
return _lookup_playlist(config, session, web_client, uri)
elif sp_link.type is spotify.LinkType.TRACK:
return list(_lookup_track(config, sp_link))
elif sp_link.type is spotify.LinkType.ALBUM:
return list(_lookup_album(config, sp_link))
elif sp_link.type is spotify.LinkType.ARTIST:
with utils.time_logger("Artist lookup"):
return list(_lookup_artist(config, sp_link))
elif sp_link.type is spotify.LinkType.PLAYLIST:
return list(_lookup_playlist(config, sp_link))
elif sp_link.type is spotify.LinkType.STARRED:
return list(reversed(list(_lookup_playlist(config, sp_link))))
else:
logger.info(
f'Failed to lookup "{uri}": Cannot handle {repr(sp_link.type)}'
Expand Down Expand Up @@ -86,11 +85,10 @@ def _lookup_artist(config, sp_link):
yield track


def _lookup_playlist(config, sp_link):
sp_playlist = sp_link.as_playlist()
sp_playlist.load(config["timeout"])
for sp_track in sp_playlist.tracks:
sp_track.load(config["timeout"])
track = translator.to_track(sp_track, bitrate=config["bitrate"])
if track is not None:
yield track
def _lookup_playlist(config, session, web_client, uri):
playlist = playlists.playlist_lookup(
session, web_client, uri, config["bitrate"]
)
if playlist is None:
raise spotify.Error("Playlist Web API lookup failed")
return playlist.tracks
4 changes: 2 additions & 2 deletions mopidy_spotify/playback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import logging
import threading

import spotify

from mopidy import audio, backend

import spotify

logger = logging.getLogger(__name__)


Expand Down
151 changes: 71 additions & 80 deletions mopidy_spotify/playlists.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,74 @@
import logging

import spotify

from mopidy import backend

import spotify
from mopidy_spotify import translator, utils

_sp_links = {}

logger = logging.getLogger(__name__)


class SpotifyPlaylistsProvider(backend.PlaylistsProvider):
def __init__(self, backend):
self._backend = backend
self._timeout = self._backend._config["spotify"]["timeout"]
self._loaded = False

def as_list(self):
with utils.time_logger("playlists.as_list()"):
with utils.time_logger("playlists.as_list()", logging.INFO):
if not self._loaded:
return []

return list(self._get_flattened_playlist_refs())

def _get_flattened_playlist_refs(self):
if self._backend._session is None:
return

if self._backend._session.playlist_container is None:
if not self._backend._web_client.logged_in:
return

username = self._backend._session.user_name
folders = []

for sp_playlist in self._backend._session.playlist_container:
if isinstance(sp_playlist, spotify.PlaylistFolder):
if sp_playlist.type is spotify.PlaylistType.START_FOLDER:
folders.append(sp_playlist.name)
elif sp_playlist.type is spotify.PlaylistType.END_FOLDER:
folders.pop()
continue

web_client = self._backend._web_client
for web_playlist in web_client.get_user_playlists():
playlist_ref = translator.to_playlist_ref(
sp_playlist, folders=folders, username=username
web_playlist, web_client.user_id
)
if playlist_ref is not None:
yield playlist_ref

def get_items(self, uri):
with utils.time_logger(f"playlist.get_items({uri})"):
with utils.time_logger(f"playlist.get_items({uri})", logging.INFO):
return self._get_playlist(uri, as_items=True)

def lookup(self, uri):
with utils.time_logger(f"playlists.lookup({uri})"):
with utils.time_logger(f"playlists.lookup({uri})", logging.DEBUG):
return self._get_playlist(uri)

def _get_playlist(self, uri, as_items=False):
try:
sp_playlist = self._backend._session.get_playlist(uri)
except spotify.Error as exc:
logger.debug(f"Failed to lookup Spotify URI {uri}: {exc}")
return

if not sp_playlist.is_loaded:
logger.debug(f"Waiting for Spotify playlist to load: {sp_playlist}")
sp_playlist.load(self._timeout)

username = self._backend._session.user_name
return translator.to_playlist(
sp_playlist,
username=username,
bitrate=self._backend._bitrate,
as_items=as_items,
return playlist_lookup(
self._backend._session,
self._backend._web_client,
uri,
self._backend._bitrate,
as_items,
)

def refresh(self):
pass # Not needed as long as we don't cache anything.
if not self._backend._web_client.logged_in:
return

with utils.time_logger("Refresh Playlists", logging.INFO):
_sp_links.clear()
self._backend._web_client.clear_cache()
count = 0
for playlist_ref in self._get_flattened_playlist_refs():
self._get_playlist(playlist_ref.uri)
count = count + 1
logger.info(f"Refreshed {count} playlists")

self._loaded = True

def create(self, name):
try:
sp_playlist = self._backend._session.playlist_container.add_new_playlist(
name
)
except ValueError as exc:
logger.warning(
f'Failed creating new Spotify playlist "{name}": {exc}'
)
except spotify.Error:
logger.warning(f'Failed creating new Spotify playlist "{name}"')
else:
username = self._backend._session.user_name
return translator.to_playlist(sp_playlist, username=username)
pass # TODO

def delete(self, uri):
pass # TODO
Expand All @@ -93,42 +77,49 @@ def save(self, playlist):
pass # TODO


def on_container_loaded(sp_playlist_container):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug("Spotify playlist container loaded")
def playlist_lookup(session, web_client, uri, bitrate, as_items=False):
if web_client is None or not web_client.logged_in:
return

# This event listener is also called after playlists are added, removed and
# moved, so since Mopidy currently only supports the "playlists_loaded"
# event this is the only place we need to trigger a Mopidy backend event.
backend.BackendListener.send("playlists_loaded")
logger.debug(f'Fetching Spotify playlist "{uri}"')
web_playlist = web_client.get_playlist(uri)

if web_playlist == {}:
logger.error(f"Failed to lookup Spotify playlist URI {uri}")
return

def on_playlist_added(sp_playlist_container, sp_playlist, index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
f'Spotify playlist "{sp_playlist.name}" added to index {index}'
playlist = translator.to_playlist(
web_playlist,
username=web_client.user_id,
bitrate=bitrate,
as_items=as_items,
)
if playlist is None:
return
# Store the libspotify Link for each track so they will be loaded in the
# background ready for using later.
if session.connection.state is spotify.ConnectionState.LOGGED_IN:
if as_items:
tracks = playlist
else:
tracks = playlist.tracks

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?


def on_playlist_removed(sp_playlist_container, sp_playlist, index):
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
f'Spotify playlist "{sp_playlist.name}" removed from index {index}'
)
for track in tracks:
if track.uri in _sp_links:
continue
try:
_sp_links[track.uri] = session.get_link(track.uri)
except ValueError as exc:
logger.info(f'Failed to get link "{track.uri}": {exc}')

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
return playlist


def on_playlist_moved(sp_playlist_container, sp_playlist, old_index, new_index):
def on_playlists_loaded():
# Called from the pyspotify event loop, and not in an actor context.
logger.debug(
f'Spotify playlist "{sp_playlist.name}" '
f"moved from index {old_index} to {new_index}"
)
logger.debug("Spotify playlists loaded")

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
# This event listener is also called after playlists are added, removed and
# moved, so since Mopidy currently only supports the "playlists_loaded"
# event this is the only place we need to trigger a Mopidy backend event.
backend.BackendListener.send("playlists_loaded")
Loading