class Core(pykka.ThreadingActor, audio.AudioListener, backend.BackendListener, mixer.MixerListener): library = None """The library controller. An instance of :class:`mopidy.core.LibraryController`.""" history = None """The playback history controller. An instance of :class:`mopidy.core.HistoryController`.""" mixer = None """The mixer controller. An instance of :class:`mopidy.core.MixerController`.""" playback = None """The playback controller. An instance of :class:`mopidy.core.PlaybackController`.""" playlists = None """The playlists controller. An instance of :class:`mopidy.core.PlaylistsController`.""" tracklist = None """The tracklist controller. An instance of :class:`mopidy.core.TracklistController`.""" def __init__(self, mixer=None, backends=None, audio=None): super(Core, self).__init__() self.backends = Backends(backends) self.library = LibraryController(backends=self.backends, core=self) self.history = HistoryController() self.mixer = MixerController(mixer=mixer) self.playback = PlaybackController(backends=self.backends, core=self) self.playlists = PlaylistsController(backends=self.backends, core=self) self.tracklist = TracklistController(core=self) self.audio = audio def get_uri_schemes(self): """Get list of URI schemes we can handle""" futures = [b.uri_schemes for b in self.backends] results = pykka.get_all(futures) uri_schemes = itertools.chain(*results) return sorted(uri_schemes) uri_schemes = deprecated_property(get_uri_schemes) """ .. deprecated:: 1.0 Use :meth:`get_uri_schemes` instead. """ def get_version(self): """Get version of the Mopidy core API""" return versioning.get_version() version = deprecated_property(get_version) """ .. deprecated:: 1.0 Use :meth:`get_version` instead. """ def reached_end_of_stream(self): self.playback._on_end_of_track() def stream_changed(self, uri): self.playback._on_stream_changed(uri) def state_changed(self, old_state, new_state, target_state): # XXX: This is a temporary fix for issue #232 while we wait for a more # permanent solution with the implementation of issue #234. When the # Spotify play token is lost, the Spotify backend pauses audio # playback, but mopidy.core doesn't know this, so we need to update # mopidy.core's state to match the actual state in mopidy.audio. If we # don't do this, clients will think that we're still playing. # We ignore cases when target state is set as this is buffering # updates (at least for now) and we need to get #234 fixed... if (new_state == PlaybackState.PAUSED and not target_state and self.playback.state != PlaybackState.PAUSED): self.playback.state = new_state self.playback._trigger_track_playback_paused() def playlists_loaded(self): # Forward event from backend to frontends CoreListener.send('playlists_loaded') def volume_changed(self, volume): # Forward event from mixer to frontends CoreListener.send('volume_changed', volume=volume) def mute_changed(self, mute): # Forward event from mixer to frontends CoreListener.send('mute_changed', mute=mute) def tags_changed(self, tags): if not self.audio or 'title' not in tags: return tags = self.audio.get_current_tags().get() if not tags: return # TODO: this limits us to only streams that set organization, this is # a hack to make sure we don't emit stream title changes for plain # tracks. We need a better way to decide if something is a stream. if 'title' in tags and tags['title'] and 'organization' in tags: title = tags['title'][0] self.playback._stream_title = title CoreListener.send('stream_title_changed', title=title)
class PlaybackController(object): pykka_traversable = True def __init__(self, backends, core): self.backends = backends self.core = core self._current_tl_track = None self._stream_title = None self._state = PlaybackState.STOPPED def _get_backend(self): # TODO: take in track instead track = self.get_current_track() if track is None: return None uri = track.uri uri_scheme = urlparse.urlparse(uri).scheme return self.backends.with_playback.get(uri_scheme, None) # Properties def get_current_tl_track(self): """Get the currently playing or selected track. Returns a :class:`mopidy.models.TlTrack` or :class:`None`. """ return self._current_tl_track def _set_current_tl_track(self, value): """Set the currently playing or selected track. *Internal:* This is only for use by Mopidy's test suite. """ self._current_tl_track = value current_tl_track = deprecated_property(get_current_tl_track) """ .. deprecated:: 1.0 Use :meth:`get_current_tl_track` instead. """ def get_current_track(self): """ Get the currently playing or selected track. Extracted from :meth:`get_current_tl_track` for convenience. Returns a :class:`mopidy.models.Track` or :class:`None`. """ tl_track = self.get_current_tl_track() if tl_track is not None: return tl_track.track current_track = deprecated_property(get_current_track) """ .. deprecated:: 1.0 Use :meth:`get_current_track` instead. """ def get_stream_title(self): """Get the current stream title or :class:`None`.""" return self._stream_title def get_state(self): """Get The playback state.""" return self._state def set_state(self, new_state): """Set the playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or :attr:`STOPPED`. Possible states and transitions: .. digraph:: state_transitions "STOPPED" -> "PLAYING" [ label="play" ] "STOPPED" -> "PAUSED" [ label="pause" ] "PLAYING" -> "STOPPED" [ label="stop" ] "PLAYING" -> "PAUSED" [ label="pause" ] "PLAYING" -> "PLAYING" [ label="play" ] "PAUSED" -> "PLAYING" [ label="resume" ] "PAUSED" -> "STOPPED" [ label="stop" ] """ (old_state, self._state) = (self.get_state(), new_state) logger.debug('Changing state: %s -> %s', old_state, new_state) self._trigger_playback_state_changed(old_state, new_state) state = deprecated_property(get_state, set_state) """ .. deprecated:: 1.0 Use :meth:`get_state` and :meth:`set_state` instead. """ def get_time_position(self): """Get time position in milliseconds.""" backend = self._get_backend() if backend: return backend.playback.get_time_position().get() else: return 0 time_position = deprecated_property(get_time_position) """ .. deprecated:: 1.0 Use :meth:`get_time_position` instead. """ def get_volume(self): """ .. deprecated:: 1.0 Use :meth:`core.mixer.get_volume() <mopidy.core.MixerController.get_volume>` instead. """ warnings.warn('playback.get_volume() is deprecated', DeprecationWarning) return self.core.mixer.get_volume() def set_volume(self, volume): """ .. deprecated:: 1.0 Use :meth:`core.mixer.set_volume() <mopidy.core.MixerController.set_volume>` instead. """ warnings.warn('playback.set_volume() is deprecated', DeprecationWarning) return self.core.mixer.set_volume(volume) volume = deprecated_property(get_volume, set_volume) """ .. deprecated:: 1.0 Use :meth:`core.mixer.get_volume() <mopidy.core.MixerController.get_volume>` and :meth:`core.mixer.set_volume() <mopidy.core.MixerController.set_volume>` instead. """ def get_mute(self): """ .. deprecated:: 1.0 Use :meth:`core.mixer.get_mute() <mopidy.core.MixerController.get_mute>` instead. """ warnings.warn('playback.get_mute() is deprecated', DeprecationWarning) return self.core.mixer.get_mute() def set_mute(self, mute): """ .. deprecated:: 1.0 Use :meth:`core.mixer.set_mute() <mopidy.core.MixerController.set_mute>` instead. """ warnings.warn('playback.set_mute() is deprecated', DeprecationWarning) return self.core.mixer.set_mute(mute) mute = deprecated_property(get_mute, set_mute) """ .. deprecated:: 1.0 Use :meth:`core.mixer.get_mute() <mopidy.core.MixerController.get_mute>` and :meth:`core.mixer.set_mute() <mopidy.core.MixerController.set_mute>` instead. """ # Methods # TODO: remove this. def _change_track(self, tl_track, on_error_step=1): """ Change to the given track, keeping the current playback state. :param tl_track: track to change to :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` :param on_error_step: direction to step at play error, 1 for next track (default), -1 for previous track. **INTERNAL** :type on_error_step: int, -1 or 1 """ old_state = self.get_state() self.stop() self._set_current_tl_track(tl_track) if old_state == PlaybackState.PLAYING: self._play(on_error_step=on_error_step) elif old_state == PlaybackState.PAUSED: self.pause() # TODO: this is not really end of track, this is on_need_next_track def _on_end_of_track(self): """ Tell the playback controller that end of track is reached. Used by event handler in :class:`mopidy.core.Core`. """ if self.get_state() == PlaybackState.STOPPED: return original_tl_track = self.get_current_tl_track() next_tl_track = self.core.tracklist.eot_track(original_tl_track) if next_tl_track: self._change_track(next_tl_track) else: self.stop() self._set_current_tl_track(None) self.core.tracklist._mark_played(original_tl_track) def _on_tracklist_change(self): """ Tell the playback controller that the current playlist has changed. Used by :class:`mopidy.core.TracklistController`. """ tracklist = self.core.tracklist.get_tl_tracks() if self.get_current_tl_track() not in tracklist: self.stop() self._set_current_tl_track(None) def _on_stream_changed(self, uri): self._stream_title = None def next(self): """ Change to the next track. The current playback state will be kept. If it was playing, playing will continue. If it was paused, it will still be paused, etc. """ original_tl_track = self.get_current_tl_track() next_tl_track = self.core.tracklist.next_track(original_tl_track) if next_tl_track: # TODO: switch to: # backend.play(track) # wait for state change? self._change_track(next_tl_track) else: self.stop() self._set_current_tl_track(None) self.core.tracklist._mark_played(original_tl_track) def pause(self): """Pause playback.""" backend = self._get_backend() if not backend or backend.playback.pause().get(): # TODO: switch to: # backend.track(pause) # wait for state change? self.set_state(PlaybackState.PAUSED) self._trigger_track_playback_paused() def play(self, tl_track=None): """ Play the given track, or if the given track is :class:`None`, play the currently active track. :param tl_track: track to play :type tl_track: :class:`mopidy.models.TlTrack` or :class:`None` """ self._play(tl_track, on_error_step=1) def _play(self, tl_track=None, on_error_step=1): if tl_track is None: if self.get_state() == PlaybackState.PAUSED: return self.resume() if self.get_current_tl_track() is not None: tl_track = self.get_current_tl_track() else: if on_error_step == 1: tl_track = self.core.tracklist.next_track(tl_track) elif on_error_step == -1: tl_track = self.core.tracklist.previous_track(tl_track) if tl_track is None: return assert tl_track in self.core.tracklist.get_tl_tracks() # TODO: switch to: # backend.play(track) # wait for state change? if self.get_state() == PlaybackState.PLAYING: self.stop() self._set_current_tl_track(tl_track) self.set_state(PlaybackState.PLAYING) backend = self._get_backend() success = False if backend: backend.playback.prepare_change() try: success = (backend.playback.change_track(tl_track.track).get() and backend.playback.play().get()) except TypeError: logger.error( '%s needs to be updated to work with this ' 'version of Mopidy.', backend) if success: self.core.tracklist._mark_playing(tl_track) self.core.history._add_track(tl_track.track) # TODO: replace with stream-changed self._trigger_track_playback_started() else: self.core.tracklist._mark_unplayable(tl_track) if on_error_step == 1: # TODO: can cause an endless loop for single track repeat. self.next() elif on_error_step == -1: self.previous() def previous(self): """ Change to the previous track. The current playback state will be kept. If it was playing, playing will continue. If it was paused, it will still be paused, etc. """ tl_track = self.get_current_tl_track() # TODO: switch to: # self.play(....) # wait for state change? self._change_track(self.core.tracklist.previous_track(tl_track), on_error_step=-1) def resume(self): """If paused, resume playing the current track.""" if self.get_state() != PlaybackState.PAUSED: return backend = self._get_backend() if backend and backend.playback.resume().get(): self.set_state(PlaybackState.PLAYING) # TODO: trigger via gst messages self._trigger_track_playback_resumed() # TODO: switch to: # backend.resume() # wait for state change? def seek(self, time_position): """ Seeks to time position given in milliseconds. :param time_position: time position in milliseconds :type time_position: int :rtype: :class:`True` if successful, else :class:`False` """ if not self.core.tracklist.tracks: return False if self.current_track and self.current_track.length is None: return False if self.get_state() == PlaybackState.STOPPED: self.play() if time_position < 0: time_position = 0 elif time_position > self.current_track.length: self.next() return True backend = self._get_backend() if not backend: return False success = backend.playback.seek(time_position).get() if success: self._trigger_seeked(time_position) return success def stop(self): """Stop playing.""" if self.get_state() != PlaybackState.STOPPED: backend = self._get_backend() time_position_before_stop = self.get_time_position() if not backend or backend.playback.stop().get(): self.set_state(PlaybackState.STOPPED) self._trigger_track_playback_ended(time_position_before_stop) def _trigger_track_playback_paused(self): logger.debug('Triggering track playback paused event') if self.current_track is None: return listener.CoreListener.send('track_playback_paused', tl_track=self.get_current_tl_track(), time_position=self.get_time_position()) def _trigger_track_playback_resumed(self): logger.debug('Triggering track playback resumed event') if self.current_track is None: return listener.CoreListener.send('track_playback_resumed', tl_track=self.get_current_tl_track(), time_position=self.get_time_position()) def _trigger_track_playback_started(self): logger.debug('Triggering track playback started event') if self.get_current_tl_track() is None: return listener.CoreListener.send('track_playback_started', tl_track=self.get_current_tl_track()) def _trigger_track_playback_ended(self, time_position_before_stop): logger.debug('Triggering track playback ended event') if self.get_current_tl_track() is None: return listener.CoreListener.send('track_playback_ended', tl_track=self.get_current_tl_track(), time_position=time_position_before_stop) def _trigger_playback_state_changed(self, old_state, new_state): logger.debug('Triggering playback state change event') listener.CoreListener.send('playback_state_changed', old_state=old_state, new_state=new_state) def _trigger_seeked(self, time_position): logger.debug('Triggering seeked event') listener.CoreListener.send('seeked', time_position=time_position)
class PlaylistsController(object): pykka_traversable = True def __init__(self, backends, core): self.backends = backends self.core = core def as_list(self): """ Get a list of the currently available playlists. Returns a list of :class:`~mopidy.models.Ref` objects referring to the playlists. In other words, no information about the playlists' content is given. :rtype: list of :class:`mopidy.models.Ref` .. versionadded:: 1.0 """ futures = { b.actor_ref.actor_class.__name__: b.playlists.as_list() for b in set(self.backends.with_playlists.values()) } results = [] for backend_name, future in futures.items(): try: results.extend(future.get()) except NotImplementedError: logger.warning( '%s does not implement playlists.as_list(). ' 'Please upgrade it.', backend_name) return results def get_items(self, uri): """ Get the items in a playlist specified by ``uri``. Returns a list of :class:`~mopidy.models.Ref` objects referring to the playlist's items. If a playlist with the given ``uri`` doesn't exist, it returns :class:`None`. :rtype: list of :class:`mopidy.models.Ref`, or :class:`None` .. versionadded:: 1.0 """ uri_scheme = urlparse.urlparse(uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if backend: return backend.playlists.get_items(uri).get() def get_playlists(self, include_tracks=True): """ Get the available playlists. :rtype: list of :class:`mopidy.models.Playlist` .. versionchanged:: 1.0 If you call the method with ``include_tracks=False``, the :attr:`~mopidy.models.Playlist.last_modified` field of the returned playlists is no longer set. .. deprecated:: 1.0 Use :meth:`as_list` and :meth:`get_items` instead. """ playlist_refs = self.as_list() if include_tracks: playlists = {r.uri: self.lookup(r.uri) for r in playlist_refs} # Use the playlist name from as_list() because it knows about any # playlist folder hierarchy, which lookup() does not. return [ playlists[r.uri].copy(name=r.name) for r in playlist_refs if playlists[r.uri] is not None ] else: return [Playlist(uri=r.uri, name=r.name) for r in playlist_refs] playlists = deprecated_property(get_playlists) """ .. deprecated:: 1.0 Use :meth:`as_list` and :meth:`get_items` instead. """ 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` """ if uri_scheme in self.backends.with_playlists: backend = self.backends.with_playlists[uri_scheme] else: # TODO: this fallback looks suspicious backend = list(self.backends.with_playlists.values())[0] playlist = backend.playlists.create(name).get() listener.CoreListener.send('playlist_changed', playlist=playlist) return playlist def delete(self, uri): """ Delete playlist identified by the URI. If the URI doesn't match the URI schemes handled by the current backends, nothing happens. :param uri: URI of the playlist to delete :type uri: string """ uri_scheme = urlparse.urlparse(uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if backend: backend.playlists.delete(uri).get() def filter(self, criteria=None, **kwargs): """ Filter playlists by the given criterias. Examples:: # Returns track with name 'a' filter({'name': 'a'}) filter(name='a') # Returns track with URI 'xyz' filter({'uri': 'xyz'}) filter(uri='xyz') # Returns track with name 'a' and URI 'xyz' filter({'name': 'a', 'uri': 'xyz'}) filter(name='a', uri='xyz') :param criteria: one or more criteria to match by :type criteria: dict :rtype: list of :class:`mopidy.models.Playlist` .. deprecated:: 1.0 Use :meth:`as_list` and filter yourself. """ criteria = criteria or kwargs matches = self.playlists for (key, value) in criteria.iteritems(): matches = filter(lambda p: getattr(p, key) == value, matches) return matches 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 = urlparse.urlparse(uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if backend: return backend.playlists.lookup(uri).get() else: return None def refresh(self, uri_scheme=None): """ Refresh the playlists in :attr:`playlists`. If ``uri_scheme`` is :class:`None`, all backends are asked to refresh. If ``uri_scheme`` is an URI scheme handled by a backend, only that backend is asked to refresh. If ``uri_scheme`` doesn't match any current backend, nothing happens. :param uri_scheme: limit to the backend matching the URI scheme :type uri_scheme: string """ if uri_scheme is None: futures = [ b.playlists.refresh() for b in self.backends.with_playlists.values() ] pykka.get_all(futures) listener.CoreListener.send('playlists_loaded') else: backend = self.backends.with_playlists.get(uri_scheme, None) if backend: backend.playlists.refresh().get() listener.CoreListener.send('playlists_loaded') 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` """ if playlist.uri is None: return uri_scheme = urlparse.urlparse(playlist.uri).scheme backend = self.backends.with_playlists.get(uri_scheme, None) if backend: playlist = backend.playlists.save(playlist).get() listener.CoreListener.send('playlist_changed', playlist=playlist) return playlist
class TracklistController(object): pykka_traversable = True def __init__(self, core): self.core = core self._next_tlid = 0 self._tl_tracks = [] self._version = 0 self._shuffled = [] # Properties def get_tl_tracks(self): """Get tracklist as list of :class:`mopidy.models.TlTrack`.""" return self._tl_tracks[:] tl_tracks = deprecated_property(get_tl_tracks) """ .. deprecated:: 1.0 Use :meth:`get_tl_tracks` instead. """ def get_tracks(self): """Get tracklist as list of :class:`mopidy.models.Track`.""" return [tl_track.track for tl_track in self._tl_tracks] tracks = deprecated_property(get_tracks) """ .. deprecated:: 1.0 Use :meth:`get_tracks` instead. """ def get_length(self): """Get length of the tracklist.""" return len(self._tl_tracks) length = deprecated_property(get_length) """ .. deprecated:: 1.0 Use :meth:`get_length` instead. """ def get_version(self): """ Get the tracklist version. Integer which is increased every time the tracklist is changed. Is not reset before Mopidy is restarted. """ return self._version def _increase_version(self): self._version += 1 self.core.playback._on_tracklist_change() self._trigger_tracklist_changed() version = deprecated_property(get_version) """ .. deprecated:: 1.0 Use :meth:`get_version` instead. """ def get_consume(self): """Get consume mode. :class:`True` Tracks are removed from the tracklist when they have been played. :class:`False` Tracks are not removed from the tracklist. """ return getattr(self, '_consume', False) def set_consume(self, value): """Set consume mode. :class:`True` Tracks are removed from the tracklist when they have been played. :class:`False` Tracks are not removed from the tracklist. """ if self.get_consume() != value: self._trigger_options_changed() return setattr(self, '_consume', value) consume = deprecated_property(get_consume, set_consume) """ .. deprecated:: 1.0 Use :meth:`get_consume` and :meth:`set_consume` instead. """ def get_random(self): """Get random mode. :class:`True` Tracks are selected at random from the tracklist. :class:`False` Tracks are played in the order of the tracklist. """ return getattr(self, '_random', False) def set_random(self, value): """Set random mode. :class:`True` Tracks are selected at random from the tracklist. :class:`False` Tracks are played in the order of the tracklist. """ if self.get_random() != value: self._trigger_options_changed() if value: self._shuffled = self.get_tl_tracks() random.shuffle(self._shuffled) return setattr(self, '_random', value) random = deprecated_property(get_random, set_random) """ .. deprecated:: 1.0 Use :meth:`get_random` and :meth:`set_random` instead. """ def get_repeat(self): """ Get repeat mode. :class:`True` The tracklist is played repeatedly. :class:`False` The tracklist is played once. """ return getattr(self, '_repeat', False) def set_repeat(self, value): """ Set repeat mode. To repeat a single track, set both ``repeat`` and ``single``. :class:`True` The tracklist is played repeatedly. :class:`False` The tracklist is played once. """ if self.get_repeat() != value: self._trigger_options_changed() return setattr(self, '_repeat', value) repeat = deprecated_property(get_repeat, set_repeat) """ .. deprecated:: 1.0 Use :meth:`get_repeat` and :meth:`set_repeat` instead. """ def get_single(self): """ Get single mode. :class:`True` Playback is stopped after current song, unless in ``repeat`` mode. :class:`False` Playback continues after current song. """ return getattr(self, '_single', False) def set_single(self, value): """ Set single mode. :class:`True` Playback is stopped after current song, unless in ``repeat`` mode. :class:`False` Playback continues after current song. """ if self.get_single() != value: self._trigger_options_changed() return setattr(self, '_single', value) single = deprecated_property(get_single, set_single) """ .. deprecated:: 1.0 Use :meth:`get_single` and :meth:`set_single` instead. """ # Methods def index(self, tl_track): """ The position of the given track in the tracklist. :param tl_track: the track to find the index of :type tl_track: :class:`mopidy.models.TlTrack` :rtype: :class:`int` or :class:`None` """ try: return self._tl_tracks.index(tl_track) except ValueError: 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` """ 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 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` """ if not self.get_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.get_tl_tracks() random.shuffle(self._shuffled) if self.get_random(): try: return self._shuffled[0] except IndexError: return None if tl_track is None: return self.get_tl_tracks()[0] next_index = self.index(tl_track) + 1 if self.get_repeat(): next_index %= len(self.get_tl_tracks()) try: return self.get_tl_tracks()[next_index] except IndexError: 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` """ 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 return self.get_tl_tracks()[position - 1] def add(self, tracks=None, at_position=None, uri=None, uris=None): """ Add the track or list of tracks to the tracklist. If ``uri`` is given instead of ``tracks``, the URI is looked up in the library and the resulting tracks are added to the tracklist. If ``uris`` is given instead of ``tracks``, the URIs are looked up in the library and the resulting tracks are added to the tracklist. If ``at_position`` is given, the tracks placed at the given position in the tracklist. If ``at_position`` is not given, the tracks are appended to the end of the tracklist. Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param tracks: tracks to add :type tracks: list of :class:`mopidy.models.Track` :param at_position: position in tracklist to add track :type at_position: int or :class:`None` :param uri: URI for tracks to add :type uri: string :rtype: list of :class:`mopidy.models.TlTrack` .. versionadded:: 1.0 The ``uris`` argument. .. deprecated:: 1.0 The ``tracks`` and ``uri`` arguments. Use ``uris``. """ assert tracks is not None or uri is not None or uris is not None, \ 'tracks, uri or uris must be provided' if tracks is None: if uri is not None: tracks = self.core.library.lookup(uri=uri) elif uris is not None: tracks = [] track_map = self.core.library.lookup(uris=uris) for uri in uris: tracks.extend(track_map[uri]) tl_tracks = [] for track in tracks: tl_track = TlTrack(self._next_tlid, track) self._next_tlid += 1 if at_position is not None: self._tl_tracks.insert(at_position, tl_track) at_position += 1 else: self._tl_tracks.append(tl_track) tl_tracks.append(tl_track) if tl_tracks: self._increase_version() return tl_tracks def clear(self): """ Clear the tracklist. Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. """ self._tl_tracks = [] self._increase_version() def filter(self, criteria=None, **kwargs): """ Filter the tracklist by the given criterias. A criteria consists of a model field to check and a list of values to compare it against. If the model field matches one of the values, it may be returned. Only tracks that matches all the given criterias are returned. Examples:: # Returns tracks with TLIDs 1, 2, 3, or 4 (tracklist ID) filter({'tlid': [1, 2, 3, 4]}) filter(tlid=[1, 2, 3, 4]) # Returns track with IDs 1, 5, or 7 filter({'id': [1, 5, 7]}) filter(id=[1, 5, 7]) # Returns track with URIs 'xyz' or 'abc' filter({'uri': ['xyz', 'abc']}) filter(uri=['xyz', 'abc']) # Returns tracks with ID 1 and URI 'xyz' filter({'id': [1], 'uri': ['xyz']}) filter(id=[1], uri=['xyz']) # Returns track with a matching ID (1, 3 or 6) and a matching URI # ('xyz' or 'abc') filter({'id': [1, 3, 6], 'uri': ['xyz', 'abc']}) filter(id=[1, 3, 6], uri=['xyz', 'abc']) :param criteria: on or more criteria to match by :type criteria: dict, of (string, list) pairs :rtype: list of :class:`mopidy.models.TlTrack` """ criteria = criteria or kwargs matches = self._tl_tracks for (key, values) in criteria.items(): if (not isinstance(values, collections.Iterable) or isinstance(values, compat.string_types)): # Fail hard if anyone is using the <0.17 calling style raise ValueError('Filter values must be iterable: %r' % values) if key == 'tlid': matches = [ct for ct in matches if ct.tlid in values] else: matches = [ ct for ct in matches if getattr(ct.track, key) in values ] return matches def move(self, start, end, to_position): """ Move the tracks in the slice ``[start:end]`` to ``to_position``. Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param start: position of first track to move :type start: int :param end: position after last track to move :type end: int :param to_position: new position for the tracks :type to_position: int """ if start == end: end += 1 tl_tracks = self._tl_tracks assert start < end, 'start must be smaller than end' assert start >= 0, 'start must be at least zero' assert end <= len(tl_tracks), \ 'end can not be larger than tracklist length' assert to_position >= 0, 'to_position must be at least zero' assert to_position <= len(tl_tracks), \ 'to_position can not be larger than tracklist length' new_tl_tracks = tl_tracks[:start] + tl_tracks[end:] for tl_track in tl_tracks[start:end]: new_tl_tracks.insert(to_position, tl_track) to_position += 1 self._tl_tracks = new_tl_tracks self._increase_version() def remove(self, criteria=None, **kwargs): """ Remove the matching tracks from the tracklist. Uses :meth:`filter()` to lookup the tracks to remove. Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param criteria: on or more criteria to match by :type criteria: dict :rtype: list of :class:`mopidy.models.TlTrack` that was removed """ tl_tracks = self.filter(criteria, **kwargs) for tl_track in tl_tracks: position = self._tl_tracks.index(tl_track) del self._tl_tracks[position] self._increase_version() return tl_tracks def shuffle(self, start=None, end=None): """ Shuffles the entire tracklist. If ``start`` and ``end`` is given only shuffles the slice ``[start:end]``. Triggers the :meth:`mopidy.core.CoreListener.tracklist_changed` event. :param start: position of first track to shuffle :type start: int or :class:`None` :param end: position after last track to shuffle :type end: int or :class:`None` """ tl_tracks = self._tl_tracks if start is not None and end is not None: assert start < end, 'start must be smaller than end' if start is not None: assert start >= 0, 'start must be at least zero' if end is not None: assert end <= len(tl_tracks), 'end can not be larger than ' + \ 'tracklist length' before = tl_tracks[:start or 0] shuffled = tl_tracks[start:end] after = tl_tracks[end or len(tl_tracks):] random.shuffle(shuffled) self._tl_tracks = before + shuffled + after self._increase_version() def slice(self, start, end): """ Returns a slice of the tracklist, limited by the given start and end positions. :param start: position of first track to include in slice :type start: int :param end: position after last track to include in slice :type end: int :rtype: :class:`mopidy.models.TlTrack` """ return self._tl_tracks[start:end] def _mark_playing(self, tl_track): """Internal method for :class:`mopidy.core.PlaybackController`.""" if self.get_random() and tl_track in self._shuffled: self._shuffled.remove(tl_track) def _mark_unplayable(self, tl_track): """Internal method for :class:`mopidy.core.PlaybackController`.""" logger.warning('Track is not playable: %s', tl_track.track.uri) if self.get_random() and tl_track in self._shuffled: self._shuffled.remove(tl_track) def _mark_played(self, tl_track): """Internal method for :class:`mopidy.core.PlaybackController`.""" if self.consume and tl_track is not None: self.remove(tlid=[tl_track.tlid]) return True return False def _trigger_tracklist_changed(self): if self.get_random(): self._shuffled = self.get_tl_tracks() random.shuffle(self._shuffled) else: self._shuffled = [] logger.debug('Triggering event: tracklist_changed()') listener.CoreListener.send('tracklist_changed') def _trigger_options_changed(self): logger.debug('Triggering options changed event') listener.CoreListener.send('options_changed')