def create(self, name, uri_scheme=None): """ Create a new playlist. If ``uri_scheme`` matches an URI scheme handled by a current backend, that backend is asked to create the playlist. If ``uri_scheme`` is :class:`None` or doesn't match a current backend, the first backend is asked to create the playlist. All new playlists must be created by calling this method, and **not** by creating new instances of :class:`mopidy.models.Playlist`. :param name: name of the new playlist :type name: string :param uri_scheme: use the backend matching the URI scheme :type uri_scheme: string :rtype: :class:`mopidy.models.Playlist` or :class:`None` """ if uri_scheme in self.backends.with_playlists: backends = [self.backends.with_playlists[uri_scheme]] else: backends = self.backends.with_playlists.values() for backend in backends: with _backend_error_handling(backend): result = backend.playlists.create(name).get() if result is None: continue validation.check_instance(result, Playlist) listener.CoreListener.send('playlist_changed', playlist=result) return result return None
def create(self, name, uri_scheme=None): """ Create a new playlist. If ``uri_scheme`` matches an URI scheme handled by a current backend, that backend is asked to create the playlist. If ``uri_scheme`` is :class:`None` or doesn't match a current backend, the first backend is asked to create the playlist. All new playlists must be created by calling this method, and **not** by creating new instances of :class:`mopidy.models.Playlist`. :param name: name of the new playlist :type name: string :param uri_scheme: use the backend matching the URI scheme :type uri_scheme: string :rtype: :class:`mopidy.models.Playlist` or :class:`None` """ if uri_scheme in self.backends.with_playlists: backends = [self.backends.with_playlists[uri_scheme]] else: backends = self.backends.with_playlists.values() for backend in backends: with _backend_error_handling(backend): result = backend.playlists.create(name).get() if result is None: continue validation.check_instance(result, Playlist) listener.CoreListener.send("playlist_changed", playlist=result) return result return None
def _roots(self): directories = set() backends = self.backends.with_library_browse.values() futures = {b: b.library.root_directory for b in backends} for backend, future in futures.items(): with _backend_error_handling(backend): root = future.get() validation.check_instance(root, models.Ref) directories.add(root) return sorted(directories, key=operator.attrgetter("name"))
def previous_track(self, tl_track): """ Returns the track that will be played if calling :meth:`mopidy.core.PlaybackController.previous()`. For normal playback this is the previous track in the tracklist. If random and/or consume is enabled it should return the current track instead. .. deprecated:: 3.0 Use :meth:`get_previous_tlid` instead. :param tl_track: the reference track :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ deprecation.warn("core.tracklist.previous_track") tl_track is None or validation.check_instance(tl_track, TlTrack) if self.get_repeat() or self.get_consume() or self.get_random(): return tl_track position = self.index(tl_track) if position in (None, 0): return None # Since we know we are not at zero we have to be somewhere in the range # 1 - len(tracks) Thus 'position - 1' will always be within the list. return self._tl_tracks[position - 1]
def index(self, tl_track=None, tlid=None): """ The position of the given track in the tracklist. If neither *tl_track* or *tlid* is given we return the index of the currently playing track. :param tl_track: the track to find the index of :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param tlid: TLID of the track to find the index of :type tlid: :class:`int` or :class:`None` :rtype: :class:`int` or :class:`None` .. versionadded:: 1.1 The *tlid* parameter """ tl_track is None or validation.check_instance(tl_track, TlTrack) tlid is None or validation.check_integer(tlid, min=1) if tl_track is None and tlid is None: tl_track = self.core.playback.get_current_tl_track() if tl_track is not None: try: return self._tl_tracks.index(tl_track) except ValueError: pass elif tlid is not None: for i, tl_track in enumerate(self._tl_tracks): if tl_track.tlid == tlid: return i return None
def previous_track(self, tl_track): """ Returns the track that will be played if calling :meth:`mopidy.core.PlaybackController.previous()`. For normal playback this is the previous track in the tracklist. If random and/or consume is enabled it should return the current track instead. :param tl_track: the reference track :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ deprecation.warn('core.tracklist.previous_track', pending=True) tl_track is None or validation.check_instance(tl_track, TlTrack) if self.get_repeat() or self.get_consume() or self.get_random(): return tl_track position = self.index(tl_track) if position in (None, 0): return None # Since we know we are not at zero we have to be somewhere in the range # 1 - len(tracks) Thus 'position - 1' will always be within the list. return self._tl_tracks[position - 1]
def set_mute(self, mute): """Set mute state. :class:`True` to mute, :class:`False` to unmute. Returns :class:`True` if call is successful, otherwise :class:`False`. """ validation.check_boolean(mute) if self._mixer is None: return False # TODO: 2.0 return None with _mixer_error_handling(self._mixer): result = self._mixer.set_mute(bool(mute)).get() validation.check_instance(result, bool) return result return False
def play(self, tl_track=None, tlid=None): """ Play the given track, or if the given tl_track and tlid is :class:`None`, play the currently active track. Note that the track **must** already be in the tracklist. .. deprecated:: 3.0 The ``tl_track`` argument. Use ``tlid`` instead. :param tl_track: track to play :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param tlid: TLID of the track to play :type tlid: :class:`int` or :class:`None` """ if sum(o is not None for o in [tl_track, tlid]) > 1: raise ValueError('At most one of "tl_track" and "tlid" may be set') tl_track is None or validation.check_instance(tl_track, models.TlTrack) tlid is None or validation.check_integer(tlid, min=1) if tl_track: deprecation.warn("core.playback.play:tl_track_kwarg") if tl_track is None and tlid is not None: for tl_track in self.core.tracklist.get_tl_tracks(): if tl_track.tlid == tlid: break else: tl_track = None if tl_track is not None: # TODO: allow from outside tracklist, would make sense given refs? assert tl_track in self.core.tracklist.get_tl_tracks() elif tl_track is None and self.get_state() == PlaybackState.PAUSED: self.resume() return current = self._pending_tl_track or self._current_tl_track pending = tl_track or current or self.core.tracklist.next_track(None) # avoid endless loop if 'repeat' is 'true' and no track is playable # * 2 -> second run to get all playable track in a shuffled playlist count = self.core.tracklist.get_length() * 2 while pending: if self._change(pending, PlaybackState.PLAYING): break else: self.core.tracklist._mark_unplayable(pending) current = pending pending = self.core.tracklist.next_track(current) count -= 1 if not count: logger.info("No playable track in the list.") break
def save(self, playlist): """ Save the playlist. For a playlist to be saveable, it must have the ``uri`` attribute set. You must not set the ``uri`` atribute yourself, but use playlist objects returned by :meth:`create` or retrieved from :attr:`playlists`, which will always give you saveable playlists. The method returns the saved playlist. The return playlist may differ from the saved playlist. E.g. if the playlist name was changed, the returned playlist may have a different URI. The caller of this method must throw away the playlist sent to this method, and use the returned playlist instead. If the playlist's URI isn't set or doesn't match the URI scheme of a current backend, nothing is done and :class:`None` is returned. :param playlist: the playlist :type playlist: :class:`mopidy.models.Playlist` :rtype: :class:`mopidy.models.Playlist` or :class:`None` """ validation.check_instance(playlist, Playlist) if playlist.uri is None: return # TODO: log this problem? uri_scheme = urllib.parse.urlparse(playlist.uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if not backend: return None # TODO: we let AssertionError error through due to legacy tests :/ with _backend_error_handling(backend, reraise=AssertionError): playlist = backend.playlists.save(playlist).get() playlist is None or validation.check_instance(playlist, Playlist) if playlist: listener.CoreListener.send("playlist_changed", playlist=playlist) return playlist return None
def save(self, playlist): """ Save the playlist. For a playlist to be saveable, it must have the ``uri`` attribute set. You must not set the ``uri`` atribute yourself, but use playlist objects returned by :meth:`create` or retrieved from :attr:`playlists`, which will always give you saveable playlists. The method returns the saved playlist. The return playlist may differ from the saved playlist. E.g. if the playlist name was changed, the returned playlist may have a different URI. The caller of this method must throw away the playlist sent to this method, and use the returned playlist instead. If the playlist's URI isn't set or doesn't match the URI scheme of a current backend, nothing is done and :class:`None` is returned. :param playlist: the playlist :type playlist: :class:`mopidy.models.Playlist` :rtype: :class:`mopidy.models.Playlist` or :class:`None` """ validation.check_instance(playlist, Playlist) if playlist.uri is None: return # TODO: log this problem? uri_scheme = urllib.parse.urlparse(playlist.uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if not backend: return None # TODO: we let AssertionError error through due to legacy tests :/ with _backend_error_handling(backend, reraise=AssertionError): playlist = backend.playlists.save(playlist).get() playlist is None or validation.check_instance(playlist, Playlist) if playlist: listener.CoreListener.send( 'playlist_changed', playlist=playlist) return playlist return None
def set_volume(self, volume): """Set the volume. The volume is defined as an integer in range [0..100]. The volume scale is linear. Returns :class:`True` if call is successful, otherwise :class:`False`. """ validation.check_integer(volume, min=0, max=100) if self._mixer is None: return False # TODO: 2.0 return None with _mixer_error_handling(self._mixer): result = self._mixer.set_volume(volume).get() validation.check_instance(result, bool) return result return False
def play(self, tl_track=None, tlid=None): """ Play the given track, or if the given tl_track and tlid is :class:`None`, play the currently active track. Note that the track **must** already be in the tracklist. :param tl_track: track to play :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param tlid: TLID of the track to play :type tlid: :class:`int` or :class:`None` """ if sum(o is not None for o in [tl_track, tlid]) > 1: raise ValueError('At most one of "tl_track" and "tlid" may be set') tl_track is None or validation.check_instance(tl_track, models.TlTrack) tlid is None or validation.check_integer(tlid, min=1) if tl_track: deprecation.warn('core.playback.play:tl_track_kwarg', pending=True) if tl_track is None and tlid is not None: for tl_track in self.core.tracklist.get_tl_tracks(): if tl_track.tlid == tlid: break else: tl_track = None if tl_track is not None: # TODO: allow from outside tracklist, would make sense given refs? assert tl_track in self.core.tracklist.get_tl_tracks() elif tl_track is None and self.get_state() == PlaybackState.PAUSED: self.resume() return current = self._pending_tl_track or self._current_tl_track pending = tl_track or current or self.core.tracklist.next_track(None) # avoid endless loop if 'repeat' is 'true' and no track is playable # * 2 -> second run to get all playable track in a shuffled playlist count = self.core.tracklist.get_length() * 2 while pending: if self._change(pending, PlaybackState.PLAYING): break else: self.core.tracklist._mark_unplayable(pending) current = pending pending = self.core.tracklist.next_track(current) count -= 1 if not count: logger.info('No playable track in the list.') break
def _load_state(self, coverage): """ Restore state from disk. Load state from disk and restore it. Parameter ``coverage`` limits the amount of data to restore. Possible values for ``coverage`` (list of one or more of): - 'tracklist' fill the tracklist - 'mode' set tracklist properties (consume, random, repeat, single) - 'play-last' restore play state ('tracklist' also required) - 'mixer' set mixer volume and mute state - 'history' restore history :param coverage: amount of data to restore :type coverage: list of strings """ file_name = os.path.join(self._get_data_dir(), b'state.json.gz') logger.info('Loading state from %s', file_name) data = storage.load(file_name) try: # Try only once. If something goes wrong, the next start is clean. os.remove(file_name) except OSError: logger.info('Failed to delete %s', file_name) if 'state' in data: core_state = data['state'] validation.check_instance(core_state, CoreState) self.history._load_state(core_state.history, coverage) self.tracklist._load_state(core_state.tracklist, coverage) self.mixer._load_state(core_state.mixer, coverage) # playback after tracklist self.playback._load_state(core_state.playback, coverage) logger.debug('Loading state done')
def get_images(self, uris): """Lookup the images for the given URIs Backends can use this to return image URIs for any URI they know about be it tracks, albums, playlists. The lookup result is a dictionary mapping the provided URIs to lists of images. Unknown URIs or URIs the corresponding backend couldn't find anything for will simply return an empty list for that URI. :param uris: list of URIs to find images for :type uris: list of string :rtype: {uri: tuple of :class:`mopidy.models.Image`} .. versionadded:: 1.0 """ validation.check_uris(uris) futures = { backend: backend.library.get_images(backend_uris) for (backend, backend_uris) in self._get_backends_to_uris(uris).items() if backend_uris } results = {uri: tuple() for uri in uris} for backend, future in futures.items(): with _backend_error_handling(backend): if future.get() is None: continue validation.check_instance(future.get(), Mapping) for uri, images in future.get().items(): if uri not in uris: raise exceptions.ValidationError( f"Got unknown image URI: {uri}") validation.check_instances(images, models.Image) results[uri] += tuple(images) return results
def next_track(self, tl_track): """ The track that will be played if calling :meth:`mopidy.core.PlaybackController.next()`. For normal playback this is the next track in the tracklist. If repeat is enabled the next track can loop around the tracklist. When random is enabled this should be a random track, all tracks should be played once before the tracklist repeats. .. deprecated:: 3.0 Use :meth:`get_next_tlid` instead. :param tl_track: the reference track :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ deprecation.warn("core.tracklist.next_track") tl_track is None or validation.check_instance(tl_track, TlTrack) if not self._tl_tracks: return None if self.get_random() and not self._shuffled: if self.get_repeat() or not tl_track: logger.debug("Shuffling tracks") self._shuffled = self._tl_tracks[:] random.shuffle(self._shuffled) if self.get_random(): if self._shuffled: return self._shuffled[0] return None next_index = self.index(tl_track) if next_index is None: next_index = 0 else: next_index += 1 if self.get_repeat(): if self.get_consume() and len(self._tl_tracks) == 1: return None else: next_index %= len(self._tl_tracks) elif next_index >= len(self._tl_tracks): return None return self._tl_tracks[next_index]
def get_images(self, uris): """Lookup the images for the given URIs Backends can use this to return image URIs for any URI they know about be it tracks, albums, playlists... The lookup result is a dictionary mapping the provided URIs to lists of images. Unknown URIs or URIs the corresponding backend couldn't find anything for will simply return an empty list for that URI. :param uris: list of URIs to find images for :type uris: list of string :rtype: {uri: tuple of :class:`mopidy.models.Image`} .. versionadded:: 1.0 """ validation.check_uris(uris) futures = { backend: backend.library.get_images(backend_uris) for (backend, backend_uris) in self._get_backends_to_uris(uris).items() if backend_uris } results = {uri: tuple() for uri in uris} for backend, future in futures.items(): with _backend_error_handling(backend): if future.get() is None: continue validation.check_instance(future.get(), collections.Mapping) for uri, images in future.get().items(): if uri not in uris: raise exceptions.ValidationError("Got unknown image URI: %s" % uri) validation.check_instances(images, models.Image) results[uri] += tuple(images) return results
def get_mute(self): """Get mute state. :class:`True` if muted, :class:`False` unmuted, :class:`None` if unknown. """ if self._mixer is None: return None with _mixer_error_handling(self._mixer): mute = self._mixer.get_mute().get() mute is None or validation.check_instance(mute, bool) return mute return None
def next_track(self, tl_track): """ The track that will be played if calling :meth:`mopidy.core.PlaybackController.next()`. For normal playback this is the next track in the tracklist. If repeat is enabled the next track can loop around the tracklist. When random is enabled this should be a random track, all tracks should be played once before the tracklist repeats. :param tl_track: the reference track :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ deprecation.warn('core.tracklist.next_track', pending=True) tl_track is None or validation.check_instance(tl_track, TlTrack) if not self._tl_tracks: return None if self.get_random() and not self._shuffled: if self.get_repeat() or not tl_track: logger.debug('Shuffling tracks') self._shuffled = self._tl_tracks[:] random.shuffle(self._shuffled) if self.get_random(): if self._shuffled: return self._shuffled[0] return None next_index = self.index(tl_track) if next_index is None: next_index = 0 else: next_index += 1 if self.get_repeat(): if self.get_consume() and len(self._tl_tracks) == 1: return None else: next_index %= len(self._tl_tracks) elif next_index >= len(self._tl_tracks): return None return self._tl_tracks[next_index]
def play(self, tl_track=None, tlid=None): """ Play the given track, or if the given tl_track and tlid is :class:`None`, play the currently active track. Note that the track **must** already be in the tracklist. :param tl_track: track to play :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param tlid: TLID of the track to play :type tlid: :class:`int` or :class:`None` """ if sum(o is not None for o in [tl_track, tlid]) > 1: raise ValueError('At most one of "tl_track" and "tlid" may be set') tl_track is None or validation.check_instance(tl_track, models.TlTrack) tlid is None or validation.check_integer(tlid, min=1) if tl_track: deprecation.warn("core.playback.play:tl_track_kwarg", pending=True) if tl_track is None and tlid is not None: for tl_track in self.core.tracklist.get_tl_tracks(): if tl_track.tlid == tlid: break else: tl_track = None if tl_track is not None: # TODO: allow from outside tracklist, would make sense given refs? assert tl_track in self.core.tracklist.get_tl_tracks() elif tl_track is None and self.get_state() == PlaybackState.PAUSED: self.resume() return current = self._pending_tl_track or self._current_tl_track pending = tl_track or current or self.core.tracklist.next_track(None) while pending: # TODO: should we consume unplayable tracks in this loop? if self._change(pending, PlaybackState.PLAYING): break else: self.core.tracklist._mark_unplayable(pending) current = pending pending = self.core.tracklist.next_track(current)
def play(self, tl_track=None, tlid=None): """ Play the given track, or if the given tl_track and tlid is :class:`None`, play the currently active track. Note that the track **must** already be in the tracklist. :param tl_track: track to play :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param tlid: TLID of the track to play :type tlid: :class:`int` or :class:`None` """ if sum(o is not None for o in [tl_track, tlid]) > 1: raise ValueError('At most one of "tl_track" and "tlid" may be set') tl_track is None or validation.check_instance(tl_track, models.TlTrack) tlid is None or validation.check_integer(tlid, min=1) if tl_track: deprecation.warn('core.playback.play:tl_track_kwarg', pending=True) if tl_track is None and tlid is not None: for tl_track in self.core.tracklist.get_tl_tracks(): if tl_track.tlid == tlid: break else: tl_track = None if tl_track is not None: # TODO: allow from outside tracklist, would make sense given refs? assert tl_track in self.core.tracklist.get_tl_tracks() elif tl_track is None and self.get_state() == PlaybackState.PAUSED: self.resume() return current = self._pending_tl_track or self._current_tl_track pending = tl_track or current or self.core.tracklist.next_track(None) while pending: if self._change(pending, PlaybackState.PLAYING): break else: self.core.tracklist._mark_unplayable(pending) current = pending pending = self.core.tracklist.next_track(current)
def lookup(self, uri): """ Lookup playlist with given URI in both the set of playlists and in any other playlist sources. Returns :class:`None` if not found. :param uri: playlist URI :type uri: string :rtype: :class:`mopidy.models.Playlist` or :class:`None` """ uri_scheme = urllib.parse.urlparse(uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if not backend: return None with _backend_error_handling(backend): playlist = backend.playlists.lookup(uri).get() playlist is None or validation.check_instance(playlist, Playlist) return playlist return None
def eot_track(self, tl_track): """ The track that will be played after the given track. Not necessarily the same track as :meth:`next_track`. :param tl_track: the reference track :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :rtype: :class:`mopidy.models.TlTrack` or :class:`None` """ deprecation.warn('core.tracklist.eot_track', pending=True) tl_track is None or validation.check_instance(tl_track, TlTrack) if self.get_single() and self.get_repeat(): return tl_track elif self.get_single(): return None # Current difference between next and EOT handling is that EOT needs to # handle "single", with that out of the way the rest of the logic is # shared. return self.next_track(tl_track)
def play(self, tl_track=None, tlid=None): """ Play the given track, or if the given tl_track and tlid is :class:`None`, play the currently active track. Note that the track **must** already be in the tracklist. :param tl_track: track to play :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param tlid: TLID of the track to play :type tlid: :class:`int` or :class:`None` """ if sum(o is not None for o in [tl_track, tlid]) > 1: raise ValueError('At most one of "tl_track" and "tlid" may be set') tl_track is None or validation.check_instance(tl_track, models.TlTrack) tlid is None or validation.check_integer(tlid, min=0) if tl_track: deprecation.warn('core.playback.play:tl_track_kwarg', pending=True) self._play(tl_track=tl_track, tlid=tlid, on_error_step=1)
def test_check_instance_with_valid_choices(): for value, cls in ((True, bool), ("a", str), (123, int)): validation.check_instance(value, cls)
def search(self, query, uris=None, exact=False): """ Search the library for tracks where ``field`` contains ``values``. ``field`` can be one of ``uri``, ``track_name``, ``album``, ``artist``, ``albumartist``, ``composer``, ``performer``, ``track_no``, ``genre``, ``date``, ``comment``, or ``any``. If ``uris`` is given, the search is limited to results from within the URI roots. For example passing ``uris=['file:']`` will limit the search to the local backend. Examples:: # Returns results matching 'a' in any backend search({'any': ['a']}) # Returns results matching artist 'xyz' in any backend search({'artist': ['xyz']}) # Returns results matching 'a' and 'b' and artist 'xyz' in any # backend search({'any': ['a', 'b'], 'artist': ['xyz']}) # Returns results matching 'a' if within the given URI roots # "file:///media/music" and "spotify:" search({'any': ['a']}, uris=['file:///media/music', 'spotify:']) # Returns results matching artist 'xyz' and 'abc' in any backend search({'artist': ['xyz', 'abc']}) :param query: one or more queries to search for :type query: dict :param uris: zero or more URI roots to limit the search to :type uris: list of string or :class:`None` :param exact: if the search should use exact matching :type exact: :class:`bool` :rtype: list of :class:`mopidy.models.SearchResult` .. versionadded:: 1.0 The ``exact`` keyword argument. """ query = _normalize_query(query) uris is None or validation.check_uris(uris) validation.check_query(query) validation.check_boolean(exact) if not query: return [] futures = {} for backend, backend_uris in self._get_backends_to_uris(uris).items(): futures[backend] = backend.library.search(query=query, uris=backend_uris, exact=exact) # Some of our tests check for LookupError to catch bad queries. This is # silly and should be replaced with query validation before passing it # to the backends. reraise = (TypeError, LookupError) results = [] for backend, future in futures.items(): try: with _backend_error_handling(backend, reraise=reraise): result = future.get() if result is not None: validation.check_instance(result, models.SearchResult) results.append(result) except TypeError: backend_name = backend.actor_ref.actor_class.__name__ logger.warning( '%s does not implement library.search() with "exact" ' "support. Please upgrade it.", backend_name, ) return results
def search(self, query=None, uris=None, exact=False, **kwargs): """ Search the library for tracks where ``field`` contains ``values``. If ``uris`` is given, the search is limited to results from within the URI roots. For example passing ``uris=['file:']`` will limit the search to the local backend. Examples:: # Returns results matching 'a' in any backend search({'any': ['a']}) # Returns results matching artist 'xyz' in any backend search({'artist': ['xyz']}) # Returns results matching 'a' and 'b' and artist 'xyz' in any # backend search({'any': ['a', 'b'], 'artist': ['xyz']}) # Returns results matching 'a' if within the given URI roots # "file:///media/music" and "spotify:" search({'any': ['a']}, uris=['file:///media/music', 'spotify:']) # Returns results matching artist 'xyz' and 'abc' in any backend search({'artist': ['xyz', 'abc']}) :param query: one or more queries to search for :type query: dict :param uris: zero or more URI roots to limit the search to :type uris: list of string or :class:`None` :param exact: if the search should use exact matching :type exact: :class:`bool` :rtype: list of :class:`mopidy.models.SearchResult` .. versionadded:: 1.0 The ``exact`` keyword argument, which replaces :meth:`find_exact`. .. deprecated:: 1.0 Previously, if the query was empty, and the backend could support it, all available tracks were returned. This has not changed, but it is strongly discouraged. No new code should rely on this behavior. .. deprecated:: 1.1 Providing the search query via ``kwargs`` is no longer supported. """ query = _normalize_query(query or kwargs) uris is None or validation.check_uris(uris) query is None or validation.check_query(query) validation.check_boolean(exact) if kwargs: deprecation.warn('core.library.search:kwargs_query') if not query: deprecation.warn('core.library.search:empty_query') futures = {} for backend, backend_uris in self._get_backends_to_uris(uris).items(): futures[backend] = backend.library.search(query=query, uris=backend_uris, exact=exact) # Some of our tests check for LookupError to catch bad queries. This is # silly and should be replaced with query validation before passing it # to the backends. reraise = (TypeError, LookupError) results = [] for backend, future in futures.items(): try: with _backend_error_handling(backend, reraise=reraise): result = future.get() if result is not None: validation.check_instance(result, models.SearchResult) results.append(result) except TypeError: backend_name = backend.actor_ref.actor_class.__name__ logger.warning( '%s does not implement library.search() with "exact" ' 'support. Please upgrade it.', backend_name) return results
def test_check_instance_with_valid_choices(): for value, cls in ((True, bool), ('a', compat.text_type), (123, int)): validation.check_instance(value, cls)
def test_check_instance_error_message(): with raises(exceptions.ValidationError) as excinfo: validation.check_instance(1, dict) assert 'Expected a dict instance, not 1' == str(excinfo.value)
def test_check_instance_with_invalid_values(): for value, cls in (1, str), ('abc', int): with raises(exceptions.ValidationError): validation.check_instance(value, cls)
def search(self, query=None, uris=None, exact=False, **kwargs): """ Search the library for tracks where ``field`` contains ``values``. If ``uris`` is given, the search is limited to results from within the URI roots. For example passing ``uris=['file:']`` will limit the search to the local backend. Examples:: # Returns results matching 'a' in any backend search({'any': ['a']}) # Returns results matching artist 'xyz' in any backend search({'artist': ['xyz']}) # Returns results matching 'a' and 'b' and artist 'xyz' in any # backend search({'any': ['a', 'b'], 'artist': ['xyz']}) # Returns results matching 'a' if within the given URI roots # "file:///media/music" and "spotify:" search({'any': ['a']}, uris=['file:///media/music', 'spotify:']) # Returns results matching artist 'xyz' and 'abc' in any backend search({'artist': ['xyz', 'abc']}) :param query: one or more queries to search for :type query: dict :param uris: zero or more URI roots to limit the search to :type uris: list of string or :class:`None` :param exact: if the search should use exact matching :type exact: :class:`bool` :rtype: list of :class:`mopidy.models.SearchResult` .. versionadded:: 1.0 The ``exact`` keyword argument, which replaces :meth:`find_exact`. .. deprecated:: 1.0 Previously, if the query was empty, and the backend could support it, all available tracks were returned. This has not changed, but it is strongly discouraged. No new code should rely on this behavior. .. deprecated:: 1.1 Providing the search query via ``kwargs`` is no longer supported. """ query = _normalize_query(query or kwargs) uris is None or validation.check_uris(uris) query is None or validation.check_query(query) validation.check_boolean(exact) if kwargs: deprecation.warn("core.library.search:kwargs_query") if not query: deprecation.warn("core.library.search:empty_query") futures = {} for backend, backend_uris in self._get_backends_to_uris(uris).items(): futures[backend] = backend.library.search(query=query, uris=backend_uris, exact=exact) # Some of our tests check for LookupError to catch bad queries. This is # silly and should be replaced with query validation before passing it # to the backends. reraise = (TypeError, LookupError) results = [] for backend, future in futures.items(): try: with _backend_error_handling(backend, reraise=reraise): result = future.get() if result is not None: validation.check_instance(result, models.SearchResult) results.append(result) except TypeError: backend_name = backend.actor_ref.actor_class.__name__ logger.warning( '%s does not implement library.search() with "exact" ' "support. Please upgrade it.", backend_name ) return results