Skip to content

Commit 4aafc68

Browse files
committed
playlists: Use the Web API (Fixes #122, #182).
Cache playlist web API responses in a simple dict. playlists: Support Spotify's new playlist URI scheme (Fixes #215). search: uses 'from_token' market.
1 parent 4eef4d0 commit 4aafc68

23 files changed

+823
-515
lines changed

mopidy_spotify/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pathlib
22

33
import pkg_resources
4-
54
from mopidy import config, ext
65

76
__version__ = pkg_resources.get_distribution("Mopidy-Spotify").version

mopidy_spotify/backend.py

+2-20
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import threading
44

55
import pykka
6-
import spotify
7-
86
from mopidy import backend, httpclient
7+
8+
import spotify
99
from mopidy_spotify import Extension, library, playback, playlists, web
1010

1111
logger = logging.getLogger(__name__)
@@ -62,7 +62,6 @@ def on_start(self):
6262
self._config["spotify"]["client_secret"],
6363
self._config["proxy"],
6464
)
65-
6665
self._web_client.login()
6766

6867
def on_stop(self):
@@ -126,23 +125,6 @@ def on_logged_in(self):
126125
logger.info("Spotify private session activated")
127126
self._session.social.private_session = True
128127

129-
self._session.playlist_container.on(
130-
spotify.PlaylistContainerEvent.CONTAINER_LOADED,
131-
playlists.on_container_loaded,
132-
)
133-
self._session.playlist_container.on(
134-
spotify.PlaylistContainerEvent.PLAYLIST_ADDED,
135-
playlists.on_playlist_added,
136-
)
137-
self._session.playlist_container.on(
138-
spotify.PlaylistContainerEvent.PLAYLIST_REMOVED,
139-
playlists.on_playlist_removed,
140-
)
141-
self._session.playlist_container.on(
142-
spotify.PlaylistContainerEvent.PLAYLIST_MOVED,
143-
playlists.on_playlist_moved,
144-
)
145-
146128
def on_play_token_lost(self):
147129
if self._session.player.state == spotify.PlayerState.PLAYING:
148130
self.playback.pause()

mopidy_spotify/browse.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import logging
22

3-
import spotify
4-
53
from mopidy import models
4+
5+
import spotify
66
from mopidy_spotify import countries, translator
77

88
logger = logging.getLogger(__name__)

mopidy_spotify/distinct.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22

33
import spotify
4-
54
from mopidy_spotify import search
65

76
logger = logging.getLogger(__name__)

mopidy_spotify/library.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from mopidy import backend
4+
45
from mopidy_spotify import browse, distinct, images, lookup, search
56

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

3132
def lookup(self, uri):
32-
return lookup.lookup(self._config, self._backend._session, uri)
33+
return lookup.lookup(
34+
self._config, self._backend._session, self._backend._web_client, uri
35+
)
3336

3437
def search(self, query=None, uris=None, exact=False):
3538
return search.search(

mopidy_spotify/lookup.py

+13-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import logging
22

33
import spotify
4-
5-
from mopidy_spotify import translator, utils
4+
from mopidy_spotify import playlists, translator, utils, web
65

76
logger = logging.getLogger(__name__)
87

@@ -11,25 +10,25 @@
1110
]
1211

1312

14-
def lookup(config, session, uri):
13+
def lookup(config, session, web_client, uri):
1514
try:
16-
sp_link = session.get_link(uri)
15+
web_link = web.parse_uri(uri)
16+
if web_link.type != "playlist":
17+
sp_link = session.get_link(uri)
1718
except ValueError as exc:
1819
logger.info(f'Failed to lookup "{uri}": {exc}')
1920
return []
2021

2122
try:
22-
if sp_link.type is spotify.LinkType.TRACK:
23+
if web_link.type == "playlist":
24+
return _lookup_playlist(config, web_client, uri)
25+
elif sp_link.type is spotify.LinkType.TRACK:
2326
return list(_lookup_track(config, sp_link))
2427
elif sp_link.type is spotify.LinkType.ALBUM:
2528
return list(_lookup_album(config, sp_link))
2629
elif sp_link.type is spotify.LinkType.ARTIST:
2730
with utils.time_logger("Artist lookup"):
2831
return list(_lookup_artist(config, sp_link))
29-
elif sp_link.type is spotify.LinkType.PLAYLIST:
30-
return list(_lookup_playlist(config, sp_link))
31-
elif sp_link.type is spotify.LinkType.STARRED:
32-
return list(reversed(list(_lookup_playlist(config, sp_link))))
3332
else:
3433
logger.info(
3534
f'Failed to lookup "{uri}": Cannot handle {repr(sp_link.type)}'
@@ -86,11 +85,8 @@ def _lookup_artist(config, sp_link):
8685
yield track
8786

8887

89-
def _lookup_playlist(config, sp_link):
90-
sp_playlist = sp_link.as_playlist()
91-
sp_playlist.load(config["timeout"])
92-
for sp_track in sp_playlist.tracks:
93-
sp_track.load(config["timeout"])
94-
track = translator.to_track(sp_track, bitrate=config["bitrate"])
95-
if track is not None:
96-
yield track
88+
def _lookup_playlist(config, web_client, uri):
89+
playlist = playlists.playlist_lookup(web_client, uri, config["bitrate"])
90+
if playlist is None:
91+
raise spotify.Error("Playlist Web API lookup failed")
92+
return playlist.tracks

mopidy_spotify/playback.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import logging
33
import threading
44

5-
import spotify
6-
75
from mopidy import audio, backend
86

7+
import spotify
8+
99
logger = logging.getLogger(__name__)
1010

1111

mopidy_spotify/playlists.py

+31-77
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import logging
22

3-
import spotify
4-
53
from mopidy import backend
4+
65
from mopidy_spotify import translator, utils
76

7+
_cache = {}
8+
89
logger = logging.getLogger(__name__)
910

1011

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

2021
def _get_flattened_playlist_refs(self):
21-
if self._backend._session is None:
22+
if self._backend._web_client is None:
2223
return
2324

24-
if self._backend._session.playlist_container is None:
25+
if self._backend._web_client.user_id is None:
2526
return
2627

27-
username = self._backend._session.user_name
28-
folders = []
29-
30-
for sp_playlist in self._backend._session.playlist_container:
31-
if isinstance(sp_playlist, spotify.PlaylistFolder):
32-
if sp_playlist.type is spotify.PlaylistType.START_FOLDER:
33-
folders.append(sp_playlist.name)
34-
elif sp_playlist.type is spotify.PlaylistType.END_FOLDER:
35-
folders.pop()
36-
continue
37-
28+
web_client = self._backend._web_client
29+
for web_playlist in web_client.get_user_playlists(_cache):
3830
playlist_ref = translator.to_playlist_ref(
39-
sp_playlist, folders=folders, username=username
31+
web_playlist, web_client.user_id
4032
)
4133
if playlist_ref is not None:
4234
yield playlist_ref
@@ -50,41 +42,15 @@ def lookup(self, uri):
5042
return self._get_playlist(uri)
5143

5244
def _get_playlist(self, uri, as_items=False):
53-
try:
54-
sp_playlist = self._backend._session.get_playlist(uri)
55-
except spotify.Error as exc:
56-
logger.debug(f"Failed to lookup Spotify URI {uri}: {exc}")
57-
return
58-
59-
if not sp_playlist.is_loaded:
60-
logger.debug(f"Waiting for Spotify playlist to load: {sp_playlist}")
61-
sp_playlist.load(self._timeout)
62-
63-
username = self._backend._session.user_name
64-
return translator.to_playlist(
65-
sp_playlist,
66-
username=username,
67-
bitrate=self._backend._bitrate,
68-
as_items=as_items,
45+
return playlist_lookup(
46+
self._backend._web_client, uri, self._backend._bitrate, as_items
6947
)
7048

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

7452
def create(self, name):
75-
try:
76-
sp_playlist = self._backend._session.playlist_container.add_new_playlist(
77-
name
78-
)
79-
except ValueError as exc:
80-
logger.warning(
81-
f'Failed creating new Spotify playlist "{name}": {exc}'
82-
)
83-
except spotify.Error:
84-
logger.warning(f'Failed creating new Spotify playlist "{name}"')
85-
else:
86-
username = self._backend._session.user_name
87-
return translator.to_playlist(sp_playlist, username=username)
53+
pass # TODO
8854

8955
def delete(self, uri):
9056
pass # TODO
@@ -93,42 +59,30 @@ def save(self, playlist):
9359
pass # TODO
9460

9561

96-
def on_container_loaded(sp_playlist_container):
97-
# Called from the pyspotify event loop, and not in an actor context.
98-
logger.debug("Spotify playlist container loaded")
99-
100-
# This event listener is also called after playlists are added, removed and
101-
# moved, so since Mopidy currently only supports the "playlists_loaded"
102-
# event this is the only place we need to trigger a Mopidy backend event.
103-
backend.BackendListener.send("playlists_loaded")
104-
105-
106-
def on_playlist_added(sp_playlist_container, sp_playlist, index):
107-
# Called from the pyspotify event loop, and not in an actor context.
108-
logger.debug(
109-
f'Spotify playlist "{sp_playlist.name}" added to index {index}'
110-
)
62+
def playlist_lookup(web_client, uri, bitrate, as_items=False):
63+
if web_client is None:
64+
return
11165

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

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

116-
def on_playlist_removed(sp_playlist_container, sp_playlist, index):
117-
# Called from the pyspotify event loop, and not in an actor context.
118-
logger.debug(
119-
f'Spotify playlist "{sp_playlist.name}" removed from index {index}'
73+
return translator.to_playlist(
74+
web_playlist,
75+
username=web_client.user_id,
76+
bitrate=bitrate,
77+
as_items=as_items,
12078
)
12179

122-
# XXX Should Mopidy support more fine grained playlist events which this
123-
# event can trigger?
124-
12580

126-
def on_playlist_moved(sp_playlist_container, sp_playlist, old_index, new_index):
81+
def on_playlists_loaded():
12782
# Called from the pyspotify event loop, and not in an actor context.
128-
logger.debug(
129-
f'Spotify playlist "{sp_playlist.name}" '
130-
f"moved from index {old_index} to {new_index}"
131-
)
83+
logger.debug("Spotify playlists loaded")
13284

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

mopidy_spotify/search.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import logging
22
import urllib.parse
33

4-
import spotify
5-
64
from mopidy import models
5+
6+
import spotify
77
from mopidy_spotify import lookup, translator
88

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

3030
if "uri" in query:
31-
return _search_by_uri(config, session, query)
31+
return _search_by_uri(config, session, web_client, query)
3232

3333
sp_query = translator.sp_search_query(query)
3434
if not sp_query:
@@ -62,7 +62,7 @@ def search(
6262
params={
6363
"q": sp_query,
6464
"limit": search_count,
65-
"market": web_client.user_country,
65+
"market": "from_token",
6666
"type": ",".join(types),
6767
},
6868
)
@@ -105,10 +105,10 @@ def search(
105105
)
106106

107107

108-
def _search_by_uri(config, session, query):
108+
def _search_by_uri(config, session, web_client, query):
109109
tracks = []
110110
for uri in query["uri"]:
111-
tracks += lookup.lookup(config, session, uri)
111+
tracks += lookup.lookup(config, session, web_client, uri)
112112

113113
uri = "spotify:search"
114114
if len(query["uri"]) == 1:

0 commit comments

Comments
 (0)