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 1 commit
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
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