from copy import copy import logging import random import time from mopidy.models import Playlist logger = logging.getLogger('backends.base') class BaseBackend(object): #: The current playlist controller. An instance of #: :class:`BaseCurrentPlaylistController`. current_playlist = None #: The library controller. An instance of :class:`BaseLibraryController`. library = None #: The playback controller. An instance of :class:`BasePlaybackController`. playback = None #: The stored playlists controller. An instance of #: :class:`BaseStoredPlaylistsController`. stored_playlists = None #: List of URI prefixes this backend can handle. uri_handlers = [] class BaseCurrentPlaylistController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` """ #: The current playlist version. Integer which is increased every time the #: current playlist is changed. Is not reset before the MPD server is #: restarted. version = 0 def __init__(self, backend): self.backend = backend self.playlist = Playlist() @property def playlist(self): """The currently loaded :class:`mopidy.models.Playlist`.""" return copy(self._playlist) @playlist.setter def playlist(self, new_playlist): self._playlist = new_playlist self.version += 1 def add(self, track, at_position=None): """ Add the track to the end of, or at the given position in the current playlist. :param track: track to add :type track: :class:`mopidy.models.Track` :param at_position: position in current playlist to add track :type at_position: int or :class:`None` """ raise NotImplementedError def clear(self): """Clear the current playlist.""" self.backend.playback.stop() self.playlist = Playlist() def get_by_id(self, id): """ Get track by ID. Raises :class:`KeyError` if not found. :param id: track ID :type id: int """ matches = filter(lambda t: t.id == id, self._playlist.tracks) if matches: return matches[0] else: raise KeyError('Track with ID "%s" not found' % id) def get_by_uri(self, uri): """ Get track by URI. Raises :class:`KeyError` if not found. :param uri: track URI :type uri: string """ matches = filter(lambda t: t.uri == uri, self._playlist.tracks) if matches: return matches[0] else: raise KeyError('Track with URI "%s" not found' % uri) def load(self, playlist): """ Replace the current playlist with the given playlist. :param playlist: playlist to load :type playlist: :class:`mopidy.models.Playlist` """ self.playlist = playlist def move(self, start, end, to_position): """ Move the tracks in the slice ``[start:end]`` to ``to_position``. :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 """ tracks = self.playlist.tracks new_tracks = tracks[:start] + tracks[end:] for track in tracks[start:end]: new_tracks.insert(to_position, track) to_position += 1 self.playlist = self.playlist.with_(tracks=new_tracks) def remove(self, track): """ Remove the track from the current playlist. :param track: track to remove :type track: :class:`mopidy.models.Track` """ tracks = self.playlist.tracks position = tracks.index(track) del tracks[position] self.playlist = self.playlist.with_(tracks=tracks) def shuffle(self, start=None, end=None): """ Shuffles the entire playlist. If ``start`` and ``end`` is given only shuffles the slice ``[start:end]``. :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` """ tracks = self.playlist.tracks before = tracks[:start or 0] shuffled = tracks[start:end] after = tracks[end or len(tracks):] random.shuffle(shuffled) self.playlist = self.playlist.with_(tracks=before+shuffled+after) class BaseLibraryController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` """ def __init__(self, backend): self.backend = backend def find_exact(self, type, query): """ Find tracks in the library where ``type`` matches ``query`` exactly. :param type: 'title', 'artist', or 'album' :type type: string :param query: the search query :type query: string :rtype: list of :class:`mopidy.models.Track` """ raise NotImplementedError def lookup(self, uri): """ Lookup track with given URI. :param uri: track URI :type uri: string :rtype: :class:`mopidy.models.Track` """ raise NotImplementedError def refresh(self, uri=None): """ Refresh library. Limit to URI and below if an URI is given. :param uri: directory or track URI :type uri: string """ raise NotImplementedError def search(self, type, query): """ Search the library for tracks where ``type`` contains ``query``. :param type: 'title', 'artist', 'album', or 'uri' :type type: string :param query: the search query :type query: string :rtype: list of :class:`mopidy.models.Track` """ raise NotImplementedError class BasePlaybackController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` """ #: Constant representing the paused state. PAUSED = u'paused' #: Constant representing the playing state. PLAYING = u'playing' #: Constant representing the stopped state. STOPPED = u'stopped' #: :class:`True` #: Tracks are removed from the playlist when they have been played. #: :class:`False` #: Tracks are not removed from the playlist. consume = False #: The currently playing or selected :class:`mopidy.models.Track`. current_track = None #: :class:`True` #: Tracks are selected at random from the playlist. #: :class:`False` #: Tracks are played in the order of the playlist. random = False #: :class:`True` #: The current track is played repeatedly. #: :class:`False` #: The current track is played once. repeat = False #: The audio volume as an int in the range [0, 100]. :class:`None` if #: unknown. volume = None def __init__(self, backend): self.backend = backend self._state = self.STOPPED @property def next_track(self): """The next :class:`mopidy.models.Track` in the playlist.""" if self.current_track is None: return None try: return self.backend.current_playlist.playlist.tracks[ self.playlist_position + 1] except IndexError: return None @property def playlist_position(self): """The position in the current playlist.""" if self.current_track is None: return None try: return self.backend.current_playlist.playlist.tracks.index( self.current_track) except ValueError: return None @property def previous_track(self): """The previous :class:`mopidy.models.Track` in the playlist.""" if self.current_track is None: return None try: return self.backend.current_playlist.playlist.tracks[ self.playlist_position - 1] except IndexError: return None @property def state(self): """ The playback state. Must be :attr:`PLAYING`, :attr:`PAUSED`, or :attr:`STOPPED`. """ return self._state @state.setter def state(self, new_state): (old_state, self._state) = (self.state, new_state) logger.debug(u'Changing state: %s -> %s', old_state, new_state) if (old_state in (self.PLAYING, self.STOPPED) and new_state == self.PLAYING): self._play_time_start() elif old_state == self.PLAYING and new_state == self.PAUSED: self._play_time_pause() elif old_state == self.PAUSED and new_state == self.PLAYING: self._play_time_resume() @property def time_position(self): """Time position in milliseconds.""" if self.state == self.PLAYING: time_since_started = int(time.time()) - self._play_time_started return self._play_time_accumulated + time_since_started elif self.state == self.PAUSED: return self._play_time_accumulated elif self.state == self.STOPPED: return 0 def _play_time_start(self): self._play_time_accumulated = 0 self._play_time_started = int(time.time()) def _play_time_pause(self): time_since_started = int(time.time()) - self._play_time_started self._play_time_accumulated += time_since_started def _play_time_resume(self): self._play_time_started = int(time.time()) def new_playlist_loaded_callback(self): """Tell the playback controller that a new playlist has been loaded.""" self.current_track = None if self.state == self.PLAYING: if self.backend.current_playlist.playlist.length > 0: self.play(self.backend.current_playlist.playlist.tracks[0]) else: self.stop() def next(self): """Play the next track.""" if self.next_track is not None and self._next(self.next_track): self.current_track = self.next_track self.state = self.PLAYING def _next(self, track): return self._play(track) def pause(self): """Pause playback.""" if self.state == self.PLAYING and self._pause(): self.state = self.PAUSED def _pause(self): raise NotImplementedError def play(self, track=None): """ Play the given track or the currently active track. :param track: track to play :type track: :class:`mopidy.models.Track` or :class:`None` """ if self.state == self.PAUSED and track is None: return self.resume() if track is not None and self._play(track): self.current_track = track self.state = self.PLAYING def _play(self, track): raise NotImplementedError def previous(self): """Play the previous track.""" if (self.previous_track is not None and self._previous(self.previous_track)): self.current_track = self.previous_track self.state = self.PLAYING def _previous(self, track): return self._play(track) def resume(self): """If paused, resume playing the current track.""" if self.state == self.PAUSED and self._resume(): self.state = self.PLAYING def _resume(self): raise NotImplementedError def seek(self, time_position): """ Seeks to time position given in milliseconds. :param time_position: time position in milliseconds :type time_position: int """ raise NotImplementedError def stop(self): """Stop playing.""" if self.state != self.STOPPED and self._stop(): self.state = self.STOPPED def _stop(self): raise NotImplementedError class BaseStoredPlaylistsController(object): """ :param backend: backend the controller is a part of :type backend: :class:`BaseBackend` """ def __init__(self, backend): self.backend = backend self._playlists = [] @property def playlists(self): """List of :class:`mopidy.models.Playlist`.""" return copy(self._playlists) def create(self, name): """ Create a new playlist. :param name: name of the new playlist :type name: string :rtype: :class:`mopidy.models.Playlist` """ raise NotImplementedError def delete(self, playlist): """ Delete playlist. :param playlist: the playlist to delete :type playlist: :class:`mopidy.models.Playlist` """ raise NotImplementedError def lookup(self, uri): """ Lookup playlist with given URI in both the set of stored playlists and in any other playlist sources. :param uri: playlist URI :type uri: string :rtype: :class:`mopidy.models.Playlist` """ raise NotImplementedError def refresh(self): """Refresh stored playlists.""" raise NotImplementedError def rename(self, playlist, new_name): """ Rename playlist. :param playlist: the playlist :type playlist: :class:`mopidy.models.Playlist` :param new_name: the new name :type new_name: string """ raise NotImplementedError def save(self, playlist): """ Save the playlist to the set of stored playlists. :param playlist: the playlist :type playlist: :class:`mopidy.models.Playlist` """ raise NotImplementedError def search(self, query): """ Search for playlists whose name contains ``query``. :param query: query to search for :type query: string :rtype: list of :class:`mopidy.models.Playlist` """ return filter(lambda p: query in p.name, self._playlists)