Skip to content

Commit

Permalink
playlists: Use the Web API (Fixes #122, #182).
Browse files Browse the repository at this point in the history
Cache playlist web API responses in a simple dict.

playlists: Support Spotify's new playlist URI scheme (Fixes #215).

search: uses 'from_token' market.
  • Loading branch information
kingosticks committed Nov 21, 2019
1 parent 4eef4d0 commit 4aafc68
Show file tree
Hide file tree
Showing 23 changed files with 823 additions and 515 deletions.
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
22 changes: 2 additions & 20 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 @@ -62,7 +62,6 @@ def on_start(self):
self._config["spotify"]["client_secret"],
self._config["proxy"],
)

self._web_client.login()

def on_stop(self):
Expand Down Expand Up @@ -126,23 +125,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
30 changes: 13 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, 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,8 @@ 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, web_client, uri):
playlist = playlists.playlist_lookup(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
108 changes: 31 additions & 77 deletions mopidy_spotify/playlists.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import logging

import spotify

from mopidy import backend

from mopidy_spotify import translator, utils

_cache = {}

logger = logging.getLogger(__name__)


Expand All @@ -18,25 +19,16 @@ def as_list(self):
return list(self._get_flattened_playlist_refs())

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

if self._backend._session.playlist_container is None:
if self._backend._web_client.user_id is None:
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(_cache):
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
Expand All @@ -50,41 +42,15 @@ def lookup(self, uri):
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._web_client, uri, self._backend._bitrate, as_items
)

def refresh(self):
pass # Not needed as long as we don't cache anything.
pass # TODO: Clear/invalidate all caches on refresh.

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 +59,30 @@ 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")

# 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")


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}'
)
def playlist_lookup(web_client, uri, bitrate, as_items=False):
if web_client is None:
return

# XXX Should Mopidy support more fine grained playlist events which this
# event can trigger?
logger.info(f'Fetching Spotify playlist "{uri}"')
web_playlist = web_client.get_playlist(uri, _cache)

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

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}'
return translator.to_playlist(
web_playlist,
username=web_client.user_id,
bitrate=bitrate,
as_items=as_items,
)

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


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")
12 changes: 6 additions & 6 deletions mopidy_spotify/search.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
import urllib.parse

import spotify

from mopidy import models

import spotify
from mopidy_spotify import lookup, translator

_SEARCH_TYPES = ["album", "artist", "track"]
Expand All @@ -28,7 +28,7 @@ def search(
return models.SearchResult(uri="spotify:search")

if "uri" in query:
return _search_by_uri(config, session, query)
return _search_by_uri(config, session, web_client, query)

sp_query = translator.sp_search_query(query)
if not sp_query:
Expand Down Expand Up @@ -62,7 +62,7 @@ def search(
params={
"q": sp_query,
"limit": search_count,
"market": web_client.user_country,
"market": "from_token",
"type": ",".join(types),
},
)
Expand Down Expand Up @@ -105,10 +105,10 @@ def search(
)


def _search_by_uri(config, session, query):
def _search_by_uri(config, session, web_client, query):
tracks = []
for uri in query["uri"]:
tracks += lookup.lookup(config, session, uri)
tracks += lookup.lookup(config, session, web_client, uri)

uri = "spotify:search"
if len(query["uri"]) == 1:
Expand Down
Loading

0 comments on commit 4aafc68

Please sign in to comment.