1 3
import contextlib
2 3
import logging
3 3
import urllib
4

5 3
from mopidy import exceptions
6 3
from mopidy.core import listener
7 3
from mopidy.internal import validation
8 3
from mopidy.models import Playlist, Ref
9

10 3
logger = logging.getLogger(__name__)
11

12

13 3
@contextlib.contextmanager
14 3
def _backend_error_handling(backend, reraise=None):
15 3
    try:
16 3
        yield
17 3
    except exceptions.ValidationError as e:
18 3
        logger.error(
19
            "%s backend returned bad data: %s",
20
            backend.actor_ref.actor_class.__name__,
21
            e,
22
        )
23 3
    except Exception as e:
24 3
        if reraise and isinstance(e, reraise):
25 3
            raise
26 3
        logger.exception(
27
            "%s backend caused an exception.",
28
            backend.actor_ref.actor_class.__name__,
29
        )
30

31

32 3
class PlaylistsController:
33 3
    def __init__(self, backends, core):
34 3
        self.backends = backends
35 3
        self.core = core
36

37 3
    def get_uri_schemes(self):
38
        """
39
        Get the list of URI schemes that support playlists.
40

41
        :rtype: list of string
42

43
        .. versionadded:: 2.0
44
        """
45 3
        return list(sorted(self.backends.with_playlists.keys()))
46

47 3
    def as_list(self):
48
        """
49
        Get a list of the currently available playlists.
50

51
        Returns a list of :class:`~mopidy.models.Ref` objects referring to the
52
        playlists. In other words, no information about the playlists' content
53
        is given.
54

55
        :rtype: list of :class:`mopidy.models.Ref`
56

57
        .. versionadded:: 1.0
58
        """
59 3
        futures = {
60
            backend: backend.playlists.as_list()
61
            for backend in set(self.backends.with_playlists.values())
62
        }
63

64 3
        results = []
65 3
        for b, future in futures.items():
66 3
            try:
67 3
                with _backend_error_handling(b, reraise=NotImplementedError):
68 3
                    playlists = future.get()
69 3
                    if playlists is not None:
70 3
                        validation.check_instances(playlists, Ref)
71 3
                        results.extend(playlists)
72 3
            except NotImplementedError:
73 3
                backend_name = b.actor_ref.actor_class.__name__
74 3
                logger.warning(
75
                    "%s does not implement playlists.as_list(). "
76
                    "Please upgrade it.",
77
                    backend_name,
78
                )
79

80 3
        return results
81

82 3
    def get_items(self, uri):
83
        """
84
        Get the items in a playlist specified by ``uri``.
85

86
        Returns a list of :class:`~mopidy.models.Ref` objects referring to the
87
        playlist's items.
88

89
        If a playlist with the given ``uri`` doesn't exist, it returns
90
        :class:`None`.
91

92
        :rtype: list of :class:`mopidy.models.Ref`, or :class:`None`
93

94
        .. versionadded:: 1.0
95
        """
96 3
        validation.check_uri(uri)
97

98 3
        uri_scheme = urllib.parse.urlparse(uri).scheme
99 3
        backend = self.backends.with_playlists.get(uri_scheme, None)
100

101 3
        if not backend:
102 3
            return None
103

104 3
        with _backend_error_handling(backend):
105 3
            items = backend.playlists.get_items(uri).get()
106 3
            items is None or validation.check_instances(items, Ref)
107 3
            return items
108

109 3
        return None
110

111 3
    def create(self, name, uri_scheme=None):
112
        """
113
        Create a new playlist.
114

115
        If ``uri_scheme`` matches an URI scheme handled by a current backend,
116
        that backend is asked to create the playlist. If ``uri_scheme`` is
117
        :class:`None` or doesn't match a current backend, the first backend is
118
        asked to create the playlist.
119

120
        All new playlists must be created by calling this method, and **not**
121
        by creating new instances of :class:`mopidy.models.Playlist`.
122

123
        :param name: name of the new playlist
124
        :type name: string
125
        :param uri_scheme: use the backend matching the URI scheme
126
        :type uri_scheme: string
127
        :rtype: :class:`mopidy.models.Playlist` or :class:`None`
128
        """
129 3
        if uri_scheme in self.backends.with_playlists:
130 3
            backends = [self.backends.with_playlists[uri_scheme]]
131
        else:
132 3
            backends = self.backends.with_playlists.values()
133

134 3
        for backend in backends:
135 3
            with _backend_error_handling(backend):
136 3
                result = backend.playlists.create(name).get()
137 3
                if result is None:
138 3
                    continue
139 3
                validation.check_instance(result, Playlist)
140 3
                listener.CoreListener.send("playlist_changed", playlist=result)
141 3
                return result
142

143 3
        return None
144

145 3
    def delete(self, uri):
146
        """
147
        Delete playlist identified by the URI.
148

149
        If the URI doesn't match the URI schemes handled by the current
150
        backends, nothing happens.
151

152
        Returns :class:`True` if deleted, :class:`False` otherwise.
153

154
        :param uri: URI of the playlist to delete
155
        :type uri: string
156
        :rtype: :class:`bool`
157

158
        .. versionchanged:: 2.2
159
            Return type defined.
160
        """
161 3
        validation.check_uri(uri)
162

163 3
        uri_scheme = urllib.parse.urlparse(uri).scheme
164 3
        backend = self.backends.with_playlists.get(uri_scheme, None)
165 3
        if not backend:
166 3
            return False
167

168 3
        success = False
169 3
        with _backend_error_handling(backend):
170 3
            success = backend.playlists.delete(uri).get()
171

172 3
        if success is None:
173
            # Return type was defined in Mopidy 2.2. Assume everything went
174
            # well if the backend doesn't report otherwise.
175 3
            success = True
176

177 3
        if success:
178 3
            listener.CoreListener.send("playlist_deleted", uri=uri)
179

180 3
        return success
181

182 3
    def lookup(self, uri):
183
        """
184
        Lookup playlist with given URI in both the set of playlists and in any
185
        other playlist sources. Returns :class:`None` if not found.
186

187
        :param uri: playlist URI
188
        :type uri: string
189
        :rtype: :class:`mopidy.models.Playlist` or :class:`None`
190
        """
191 3
        uri_scheme = urllib.parse.urlparse(uri).scheme
192 3
        backend = self.backends.with_playlists.get(uri_scheme, None)
193 3
        if not backend:
194 3
            return None
195

196 3
        with _backend_error_handling(backend):
197 3
            playlist = backend.playlists.lookup(uri).get()
198 3
            playlist is None or validation.check_instance(playlist, Playlist)
199 3
            return playlist
200

201 3
        return None
202

203
    # TODO: there is an inconsistency between library.refresh(uri) and this
204
    # call, not sure how to sort this out.
205 3
    def refresh(self, uri_scheme=None):
206
        """
207
        Refresh the playlists in :attr:`playlists`.
208

209
        If ``uri_scheme`` is :class:`None`, all backends are asked to refresh.
210
        If ``uri_scheme`` is an URI scheme handled by a backend, only that
211
        backend is asked to refresh. If ``uri_scheme`` doesn't match any
212
        current backend, nothing happens.
213

214
        :param uri_scheme: limit to the backend matching the URI scheme
215
        :type uri_scheme: string
216
        """
217
        # TODO: check: uri_scheme is None or uri_scheme?
218

219 3
        futures = {}
220 3
        backends = {}
221 3
        playlists_loaded = False
222

223 3
        for backend_scheme, backend in self.backends.with_playlists.items():
224 3
            backends.setdefault(backend, set()).add(backend_scheme)
225

226 3
        for backend, backend_schemes in backends.items():
227 3
            if uri_scheme is None or uri_scheme in backend_schemes:
228 3
                futures[backend] = backend.playlists.refresh()
229

230 3
        for backend, future in futures.items():
231 3
            with _backend_error_handling(backend):
232 3
                future.get()
233 3
                playlists_loaded = True
234

235 3
        if playlists_loaded:
236 3
            listener.CoreListener.send("playlists_loaded")
237

238 3
    def save(self, playlist):
239
        """
240
        Save the playlist.
241

242
        For a playlist to be saveable, it must have the ``uri`` attribute set.
243
        You must not set the ``uri`` atribute yourself, but use playlist
244
        objects returned by :meth:`create` or retrieved from :attr:`playlists`,
245
        which will always give you saveable playlists.
246

247
        The method returns the saved playlist. The return playlist may differ
248
        from the saved playlist. E.g. if the playlist name was changed, the
249
        returned playlist may have a different URI. The caller of this method
250
        must throw away the playlist sent to this method, and use the
251
        returned playlist instead.
252

253
        If the playlist's URI isn't set or doesn't match the URI scheme of a
254
        current backend, nothing is done and :class:`None` is returned.
255

256
        :param playlist: the playlist
257
        :type playlist: :class:`mopidy.models.Playlist`
258
        :rtype: :class:`mopidy.models.Playlist` or :class:`None`
259
        """
260 3
        validation.check_instance(playlist, Playlist)
261

262 3
        if playlist.uri is None:
263 3
            return  # TODO: log this problem?
264

265 3
        uri_scheme = urllib.parse.urlparse(playlist.uri).scheme
266 3
        backend = self.backends.with_playlists.get(uri_scheme, None)
267 3
        if not backend:
268 3
            return None
269

270
        # TODO: we let AssertionError error through due to legacy tests :/
271 3
        with _backend_error_handling(backend, reraise=AssertionError):
272 3
            playlist = backend.playlists.save(playlist).get()
273 3
            playlist is None or validation.check_instance(playlist, Playlist)
274 3
            if playlist:
275 3
                listener.CoreListener.send(
276
                    "playlist_changed", playlist=playlist
277
                )
278 3
            return playlist
279

280 3
        return None

Read our documentation on viewing source code .

Loading