From 0559213da326cbb4ccd5e2aa5bc5e547ef30f444 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 13 Sep 2012 23:40:45 +0200 Subject: [PATCH] Move backend controllers to mopidy.core --- mopidy/backends/base/__init__.py | 11 +- mopidy/backends/base/library.py | 76 --- mopidy/backends/base/playback.py | 547 ----------------- mopidy/backends/base/stored_playlists.py | 116 ---- mopidy/backends/dummy/__init__.py | 22 +- mopidy/backends/local/__init__.py | 25 +- mopidy/backends/spotify/__init__.py | 15 +- mopidy/core/__init__.py | 4 + .../base => core}/current_playlist.py | 4 +- mopidy/core/library.py | 70 +++ mopidy/core/playback.py | 548 ++++++++++++++++++ mopidy/core/stored_playlists.py | 113 ++++ mopidy/frontends/mpd/protocol/playback.py | 12 +- mopidy/frontends/mpd/protocol/status.py | 12 +- mopidy/frontends/mpris/objects.py | 17 +- tests/core/__init__.py | 0 tests/frontends/mpd/protocol/playback_test.py | 8 +- tests/frontends/mpd/status_test.py | 11 +- .../frontends/mpris/player_interface_test.py | 9 +- 19 files changed, 803 insertions(+), 817 deletions(-) create mode 100644 mopidy/core/__init__.py rename mopidy/{backends/base => core}/current_playlist.py (99%) create mode 100644 mopidy/core/library.py create mode 100644 mopidy/core/playback.py create mode 100644 mopidy/core/stored_playlists.py create mode 100644 tests/core/__init__.py diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 76c7f078..e6c8b70a 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -1,12 +1,7 @@ -import logging +from .library import BaseLibraryProvider +from .playback import BasePlaybackProvider +from .stored_playlists import BaseStoredPlaylistsProvider -from .current_playlist import CurrentPlaylistController -from .library import LibraryController, BaseLibraryProvider -from .playback import PlaybackController, BasePlaybackProvider -from .stored_playlists import (StoredPlaylistsController, - BaseStoredPlaylistsProvider) - -logger = logging.getLogger('mopidy.backends.base') class Backend(object): #: The current playlist controller. An instance of diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index 9e3afe9a..837eef49 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -1,79 +1,3 @@ -import logging - -logger = logging.getLogger('mopidy.backends.base') - -class LibraryController(object): - """ - :param backend: backend the controller is a part of - :type backend: :class:`mopidy.backends.base.Backend` - :param provider: provider the controller should use - :type provider: instance of :class:`BaseLibraryProvider` - """ - - pykka_traversable = True - - def __init__(self, backend, provider): - self.backend = backend - self.provider = provider - - def find_exact(self, **query): - """ - Search the library for tracks where ``field`` is ``values``. - - Examples:: - - # Returns results matching 'a' - find_exact(any=['a']) - # Returns results matching artist 'xyz' - find_exact(artist=['xyz']) - # Returns results matching 'a' and 'b' and artist 'xyz' - find_exact(any=['a', 'b'], artist=['xyz']) - - :param query: one or more queries to search for - :type query: dict - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.find_exact(**query) - - def lookup(self, uri): - """ - Lookup track with given URI. Returns :class:`None` if not found. - - :param uri: track URI - :type uri: string - :rtype: :class:`mopidy.models.Track` or :class:`None` - """ - return self.provider.lookup(uri) - - 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 - """ - return self.provider.refresh(uri) - - def search(self, **query): - """ - Search the library for tracks where ``field`` contains ``values``. - - Examples:: - - # Returns results matching 'a' - search(any=['a']) - # Returns results matching artist 'xyz' - search(artist=['xyz']) - # Returns results matching 'a' and 'b' and artist 'xyz' - search(any=['a', 'b'], artist=['xyz']) - - :param query: one or more queries to search for - :type query: dict - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.search(**query) - - class BaseLibraryProvider(object): """ :param backend: backend the controller is a part of diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index dfcbe8bb..d2b9edd9 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -1,550 +1,3 @@ -import logging -import random -import time - -from mopidy.listeners import BackendListener - -logger = logging.getLogger('mopidy.backends.base') - - -def option_wrapper(name, default): - def get_option(self): - return getattr(self, name, default) - def set_option(self, value): - if getattr(self, name, default) != value: - self._trigger_options_changed() - return setattr(self, name, value) - return property(get_option, set_option) - - -class PlaybackController(object): - """ - :param backend: the backend - :type backend: :class:`mopidy.backends.base.Backend` - :param provider: provider the controller should use - :type provider: instance of :class:`BasePlaybackProvider` - """ - - # pylint: disable = R0902 - # Too many instance attributes - - pykka_traversable = True - - #: 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 = option_wrapper('_consume', False) - - #: The currently playing or selected track. - #: - #: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or - #: :class:`None`. - current_cp_track = None - - #: :class:`True` - #: Tracks are selected at random from the playlist. - #: :class:`False` - #: Tracks are played in the order of the playlist. - random = option_wrapper('_random', False) - - #: :class:`True` - #: The current playlist is played repeatedly. To repeat a single track, - #: select both :attr:`repeat` and :attr:`single`. - #: :class:`False` - #: The current playlist is played once. - repeat = option_wrapper('_repeat', False) - - #: :class:`True` - #: Playback is stopped after current song, unless in :attr:`repeat` - #: mode. - #: :class:`False` - #: Playback continues after current song. - single = option_wrapper('_single', False) - - def __init__(self, backend, provider): - self.backend = backend - self.provider = provider - self._state = self.STOPPED - self._shuffled = [] - self._first_shuffle = True - self.play_time_accumulated = 0 - self.play_time_started = None - - def _get_cpid(self, cp_track): - if cp_track is None: - return None - return cp_track.cpid - - def _get_track(self, cp_track): - if cp_track is None: - return None - return cp_track.track - - @property - def current_cpid(self): - """ - The CPID (current playlist ID) of the currently playing or selected - track. - - Read-only. Extracted from :attr:`current_cp_track` for convenience. - """ - return self._get_cpid(self.current_cp_track) - - @property - def current_track(self): - """ - The currently playing or selected :class:`mopidy.models.Track`. - - Read-only. Extracted from :attr:`current_cp_track` for convenience. - """ - return self._get_track(self.current_cp_track) - - @property - def current_playlist_position(self): - """ - The position of the current track in the current playlist. - - Read-only. - """ - if self.current_cp_track is None: - return None - try: - return self.backend.current_playlist.cp_tracks.index( - self.current_cp_track) - except ValueError: - return None - - @property - def track_at_eot(self): - """ - The track that will be played at the end of the current track. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`cp_track_at_eot` for convenience. - """ - return self._get_track(self.cp_track_at_eot) - - @property - def cp_track_at_eot(self): - """ - The track that will be played at the end of the current track. - - Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). - - Not necessarily the same track as :attr:`cp_track_at_next`. - """ - # pylint: disable = R0911 - # Too many return statements - - cp_tracks = self.backend.current_playlist.cp_tracks - - if not cp_tracks: - return None - - if self.random and not self._shuffled: - if self.repeat or self._first_shuffle: - logger.debug('Shuffling tracks') - self._shuffled = cp_tracks - random.shuffle(self._shuffled) - self._first_shuffle = False - - if self.random and self._shuffled: - return self._shuffled[0] - - if self.current_cp_track is None: - return cp_tracks[0] - - if self.repeat and self.single: - return cp_tracks[self.current_playlist_position] - - if self.repeat and not self.single: - return cp_tracks[ - (self.current_playlist_position + 1) % len(cp_tracks)] - - try: - return cp_tracks[self.current_playlist_position + 1] - except IndexError: - return None - - @property - def track_at_next(self): - """ - The track that will be played if calling :meth:`next()`. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`cp_track_at_next` for convenience. - """ - return self._get_track(self.cp_track_at_next) - - @property - def cp_track_at_next(self): - """ - The track that will be played if calling :meth:`next()`. - - Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). - - For normal playback this is the next track in the playlist. If repeat - is enabled the next track can loop around the playlist. When random is - enabled this should be a random track, all tracks should be played once - before the list repeats. - """ - cp_tracks = self.backend.current_playlist.cp_tracks - - if not cp_tracks: - return None - - if self.random and not self._shuffled: - if self.repeat or self._first_shuffle: - logger.debug('Shuffling tracks') - self._shuffled = cp_tracks - random.shuffle(self._shuffled) - self._first_shuffle = False - - if self.random and self._shuffled: - return self._shuffled[0] - - if self.current_cp_track is None: - return cp_tracks[0] - - if self.repeat: - return cp_tracks[ - (self.current_playlist_position + 1) % len(cp_tracks)] - - try: - return cp_tracks[self.current_playlist_position + 1] - except IndexError: - return None - - @property - def track_at_previous(self): - """ - The track that will be played if calling :meth:`previous()`. - - Read-only. A :class:`mopidy.models.Track` extracted from - :attr:`cp_track_at_previous` for convenience. - """ - return self._get_track(self.cp_track_at_previous) - - @property - def cp_track_at_previous(self): - """ - The track that will be played if calling :meth:`previous()`. - - A two-tuple of (CPID integer, :class:`mopidy.models.Track`). - - For normal playback this is the previous track in the playlist. If - random and/or consume is enabled it should return the current track - instead. - """ - if self.repeat or self.consume or self.random: - return self.current_cp_track - - if self.current_playlist_position in (None, 0): - return None - - return self.backend.current_playlist.cp_tracks[ - self.current_playlist_position - 1] - - @property - def state(self): - """ - 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" ] - """ - 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) - - self._trigger_playback_state_changed() - - # FIXME play_time stuff assumes backend does not have a better way of - # handeling this stuff :/ - 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 = (self._current_wall_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 = self._current_wall_time - - def _play_time_pause(self): - time_since_started = self._current_wall_time - self.play_time_started - self.play_time_accumulated += time_since_started - - def _play_time_resume(self): - self.play_time_started = self._current_wall_time - - @property - def _current_wall_time(self): - return int(time.time() * 1000) - - @property - def volume(self): - return self.provider.get_volume() - - @volume.setter - def volume(self, volume): - self.provider.set_volume(volume) - - def change_track(self, cp_track, on_error_step=1): - """ - Change to the given track, keeping the current playback state. - - :param cp_track: track to change to - :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) - or :class:`None` - :param on_error_step: direction to step at play error, 1 for next - track (default), -1 for previous track - :type on_error_step: int, -1 or 1 - - """ - old_state = self.state - self.stop() - self.current_cp_track = cp_track - if old_state == self.PLAYING: - self.play(on_error_step=on_error_step) - elif old_state == self.PAUSED: - self.pause() - - def on_end_of_track(self): - """ - Tell the playback controller that end of track is reached. - """ - if self.state == self.STOPPED: - return - - original_cp_track = self.current_cp_track - - if self.cp_track_at_eot: - self._trigger_track_playback_ended() - self.play(self.cp_track_at_eot) - else: - self.stop(clear_current_track=True) - - if self.consume: - self.backend.current_playlist.remove(cpid=original_cp_track.cpid) - - def on_current_playlist_change(self): - """ - Tell the playback controller that the current playlist has changed. - - Used by :class:`mopidy.backends.base.CurrentPlaylistController`. - """ - self._first_shuffle = True - self._shuffled = [] - - if (not self.backend.current_playlist.cp_tracks or - self.current_cp_track not in - self.backend.current_playlist.cp_tracks): - self.stop(clear_current_track=True) - - 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. - """ - if self.cp_track_at_next: - self._trigger_track_playback_ended() - self.change_track(self.cp_track_at_next) - else: - self.stop(clear_current_track=True) - - def pause(self): - """Pause playback.""" - if self.provider.pause(): - self.state = self.PAUSED - self._trigger_track_playback_paused() - - def play(self, cp_track=None, on_error_step=1): - """ - Play the given track, or if the given track is :class:`None`, play the - currently active track. - - :param cp_track: track to play - :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) - or :class:`None` - :param on_error_step: direction to step at play error, 1 for next - track (default), -1 for previous track - :type on_error_step: int, -1 or 1 - """ - - if cp_track is not None: - assert cp_track in self.backend.current_playlist.cp_tracks - elif cp_track is None: - if self.state == self.PAUSED: - return self.resume() - elif self.current_cp_track is not None: - cp_track = self.current_cp_track - elif self.current_cp_track is None and on_error_step == 1: - cp_track = self.cp_track_at_next - elif self.current_cp_track is None and on_error_step == -1: - cp_track = self.cp_track_at_previous - - if cp_track is not None: - self.current_cp_track = cp_track - self.state = self.PLAYING - if not self.provider.play(cp_track.track): - # Track is not playable - if self.random and self._shuffled: - self._shuffled.remove(cp_track) - if on_error_step == 1: - self.next() - elif on_error_step == -1: - self.previous() - - if self.random and self.current_cp_track in self._shuffled: - self._shuffled.remove(self.current_cp_track) - - self._trigger_track_playback_started() - - 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. - """ - self._trigger_track_playback_ended() - self.change_track(self.cp_track_at_previous, on_error_step=-1) - - def resume(self): - """If paused, resume playing the current track.""" - if self.state == self.PAUSED and self.provider.resume(): - self.state = self.PLAYING - self._trigger_track_playback_resumed() - - 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.backend.current_playlist.tracks: - return False - - if self.state == self.STOPPED: - self.play() - elif self.state == self.PAUSED: - self.resume() - - if time_position < 0: - time_position = 0 - elif time_position > self.current_track.length: - self.next() - return True - - self.play_time_started = self._current_wall_time - self.play_time_accumulated = time_position - - success = self.provider.seek(time_position) - if success: - self._trigger_seeked() - return success - - def stop(self, clear_current_track=False): - """ - Stop playing. - - :param clear_current_track: whether to clear the current track _after_ - stopping - :type clear_current_track: boolean - """ - if self.state != self.STOPPED: - if self.provider.stop(): - self._trigger_track_playback_ended() - self.state = self.STOPPED - if clear_current_track: - self.current_cp_track = None - - def _trigger_track_playback_paused(self): - logger.debug(u'Triggering track playback paused event') - if self.current_track is None: - return - BackendListener.send('track_playback_paused', - track=self.current_track, - time_position=self.time_position) - - def _trigger_track_playback_resumed(self): - logger.debug(u'Triggering track playback resumed event') - if self.current_track is None: - return - BackendListener.send('track_playback_resumed', - track=self.current_track, - time_position=self.time_position) - - def _trigger_track_playback_started(self): - logger.debug(u'Triggering track playback started event') - if self.current_track is None: - return - BackendListener.send('track_playback_started', - track=self.current_track) - - def _trigger_track_playback_ended(self): - logger.debug(u'Triggering track playback ended event') - if self.current_track is None: - return - BackendListener.send('track_playback_ended', - track=self.current_track, - time_position=self.time_position) - - def _trigger_playback_state_changed(self): - logger.debug(u'Triggering playback state change event') - BackendListener.send('playback_state_changed') - - def _trigger_options_changed(self): - logger.debug(u'Triggering options changed event') - BackendListener.send('options_changed') - - def _trigger_seeked(self): - logger.debug(u'Triggering seeked event') - BackendListener.send('seeked') - - class BasePlaybackProvider(object): """ :param backend: the backend diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 0ce2e196..d1d52c9a 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -1,120 +1,4 @@ from copy import copy -import logging - -logger = logging.getLogger('mopidy.backends.base') - -class StoredPlaylistsController(object): - """ - :param backend: backend the controller is a part of - :type backend: :class:`mopidy.backends.base.Backend` - :param provider: provider the controller should use - :type provider: instance of :class:`BaseStoredPlaylistsProvider` - """ - - pykka_traversable = True - - def __init__(self, backend, provider): - self.backend = backend - self.provider = provider - - @property - def playlists(self): - """ - Currently stored playlists. - - Read/write. List of :class:`mopidy.models.Playlist`. - """ - return self.provider.playlists - - @playlists.setter - def playlists(self, playlists): - self.provider.playlists = playlists - - def create(self, name): - """ - Create a new playlist. - - :param name: name of the new playlist - :type name: string - :rtype: :class:`mopidy.models.Playlist` - """ - return self.provider.create(name) - - def delete(self, playlist): - """ - Delete playlist. - - :param playlist: the playlist to delete - :type playlist: :class:`mopidy.models.Playlist` - """ - return self.provider.delete(playlist) - - def get(self, **criteria): - """ - Get playlist by given criterias from the set of stored playlists. - - Raises :exc:`LookupError` if a unique match is not found. - - Examples:: - - get(name='a') # Returns track with name 'a' - get(uri='xyz') # Returns track with URI 'xyz' - get(name='a', uri='xyz') # Returns track with name 'a' and URI - # 'xyz' - - :param criteria: one or more criteria to match by - :type criteria: dict - :rtype: :class:`mopidy.models.Playlist` - """ - matches = self.playlists - for (key, value) in criteria.iteritems(): - matches = filter(lambda p: getattr(p, key) == value, matches) - if len(matches) == 1: - return matches[0] - criteria_string = ', '.join( - ['%s=%s' % (k, v) for (k, v) in criteria.iteritems()]) - if len(matches) == 0: - raise LookupError('"%s" match no playlists' % criteria_string) - else: - raise LookupError('"%s" match multiple playlists' % criteria_string) - - 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` - """ - return self.provider.lookup(uri) - - def refresh(self): - """ - Refresh the stored playlists in - :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`. - """ - return self.provider.refresh() - - 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 - """ - return self.provider.rename(playlist, new_name) - - def save(self, playlist): - """ - Save the playlist to the set of stored playlists. - - :param playlist: the playlist - :type playlist: :class:`mopidy.models.Playlist` - """ - return self.provider.save(playlist) class BaseStoredPlaylistsProvider(object): diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 2234242c..3ada0052 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,13 +1,11 @@ from pykka.actor import ThreadingActor -from mopidy.backends.base import (Backend, CurrentPlaylistController, - PlaybackController, BasePlaybackProvider, LibraryController, - BaseLibraryProvider, StoredPlaylistsController, - BaseStoredPlaylistsProvider) +from mopidy import core +from mopidy.backends import base from mopidy.models import Playlist -class DummyBackend(ThreadingActor, Backend): +class DummyBackend(ThreadingActor, base.Backend): """ A backend which implements the backend API in the simplest way possible. Used in tests of the frontends. @@ -18,24 +16,24 @@ class DummyBackend(ThreadingActor, Backend): def __init__(self, *args, **kwargs): super(DummyBackend, self).__init__(*args, **kwargs) - self.current_playlist = CurrentPlaylistController(backend=self) + self.current_playlist = core.CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) - self.library = LibraryController(backend=self, + self.library = core.LibraryController(backend=self, provider=library_provider) playback_provider = DummyPlaybackProvider(backend=self) - self.playback = PlaybackController(backend=self, + self.playback = core.PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = DummyStoredPlaylistsProvider(backend=self) - self.stored_playlists = StoredPlaylistsController(backend=self, + self.stored_playlists = core.StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_schemes = [u'dummy'] -class DummyLibraryProvider(BaseLibraryProvider): +class DummyLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) self.dummy_library = [] @@ -55,7 +53,7 @@ class DummyLibraryProvider(BaseLibraryProvider): return Playlist() -class DummyPlaybackProvider(BasePlaybackProvider): +class DummyPlaybackProvider(base.BasePlaybackProvider): def __init__(self, *args, **kwargs): super(DummyPlaybackProvider, self).__init__(*args, **kwargs) self._volume = None @@ -83,7 +81,7 @@ class DummyPlaybackProvider(BasePlaybackProvider): self._volume = volume -class DummyStoredPlaylistsProvider(BaseStoredPlaylistsProvider): +class DummyStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): def create(self, name): playlist = Playlist(name=name) self._playlists.append(playlist) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 263d2fc2..e8d918b0 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -7,11 +7,8 @@ import shutil from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings, DATA_PATH -from mopidy.backends.base import (Backend, CurrentPlaylistController, - LibraryController, BaseLibraryProvider, PlaybackController, - BasePlaybackProvider, StoredPlaylistsController, - BaseStoredPlaylistsProvider) +from mopidy import core, settings, DATA_PATH +from mopidy.backends import base from mopidy.models import Playlist, Track, Album from mopidy.gstreamer import GStreamer @@ -27,12 +24,10 @@ if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'): DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music') -class LocalBackend(ThreadingActor, Backend): +class LocalBackend(ThreadingActor, base.Backend): """ A backend for playing music from a local music archive. - **Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local - **Dependencies:** - None @@ -47,10 +42,10 @@ class LocalBackend(ThreadingActor, Backend): def __init__(self, *args, **kwargs): super(LocalBackend, self).__init__(*args, **kwargs) - self.current_playlist = CurrentPlaylistController(backend=self) + self.current_playlist = core.CurrentPlaylistController(backend=self) library_provider = LocalLibraryProvider(backend=self) - self.library = LibraryController(backend=self, + self.library = core.LibraryController(backend=self, provider=library_provider) playback_provider = LocalPlaybackProvider(backend=self) @@ -58,7 +53,7 @@ class LocalBackend(ThreadingActor, Backend): provider=playback_provider) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) - self.stored_playlists = StoredPlaylistsController(backend=self, + self.stored_playlists = core.StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_schemes = [u'file'] @@ -72,7 +67,7 @@ class LocalBackend(ThreadingActor, Backend): self.gstreamer = gstreamer_refs[0].proxy() -class LocalPlaybackController(PlaybackController): +class LocalPlaybackController(core.PlaybackController): def __init__(self, *args, **kwargs): super(LocalPlaybackController, self).__init__(*args, **kwargs) @@ -84,7 +79,7 @@ class LocalPlaybackController(PlaybackController): return self.backend.gstreamer.get_position().get() -class LocalPlaybackProvider(BasePlaybackProvider): +class LocalPlaybackProvider(base.BasePlaybackProvider): def pause(self): return self.backend.gstreamer.pause_playback().get() @@ -109,7 +104,7 @@ class LocalPlaybackProvider(BasePlaybackProvider): self.backend.gstreamer.set_volume(volume).get() -class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): +class LocalStoredPlaylistsProvider(base.BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH @@ -182,7 +177,7 @@ class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): self._playlists.append(playlist) -class LocalLibraryProvider(BaseLibraryProvider): +class LocalLibraryProvider(base.BaseLibraryProvider): def __init__(self, *args, **kwargs): super(LocalLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 56775926..fef86280 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -3,16 +3,15 @@ import logging from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings -from mopidy.backends.base import (Backend, CurrentPlaylistController, - LibraryController, PlaybackController, StoredPlaylistsController) +from mopidy import core, settings +from mopidy.backends import base from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') BITRATES = {96: 2, 160: 0, 320: 1} -class SpotifyBackend(ThreadingActor, Backend): +class SpotifyBackend(ThreadingActor, base.Backend): """ A backend for playing music from the `Spotify `_ music streaming service. The backend uses the official `libspotify @@ -51,19 +50,19 @@ class SpotifyBackend(ThreadingActor, Backend): super(SpotifyBackend, self).__init__(*args, **kwargs) - self.current_playlist = CurrentPlaylistController(backend=self) + self.current_playlist = core.CurrentPlaylistController(backend=self) library_provider = SpotifyLibraryProvider(backend=self) - self.library = LibraryController(backend=self, + self.library = core.LibraryController(backend=self, provider=library_provider) playback_provider = SpotifyPlaybackProvider(backend=self) - self.playback = PlaybackController(backend=self, + self.playback = core.PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = SpotifyStoredPlaylistsProvider( backend=self) - self.stored_playlists = StoredPlaylistsController(backend=self, + self.stored_playlists = core.StoredPlaylistsController(backend=self, provider=stored_playlists_provider) self.uri_schemes = [u'spotify'] diff --git a/mopidy/core/__init__.py b/mopidy/core/__init__.py new file mode 100644 index 00000000..16c09665 --- /dev/null +++ b/mopidy/core/__init__.py @@ -0,0 +1,4 @@ +from .current_playlist import CurrentPlaylistController +from .library import LibraryController +from .playback import PlaybackController +from .stored_playlists import StoredPlaylistsController diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/core/current_playlist.py similarity index 99% rename from mopidy/backends/base/current_playlist.py rename to mopidy/core/current_playlist.py index d7e6c331..af06e05e 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/core/current_playlist.py @@ -5,7 +5,9 @@ import random from mopidy.listeners import BackendListener from mopidy.models import CpTrack -logger = logging.getLogger('mopidy.backends.base') + +logger = logging.getLogger('mopidy.core') + class CurrentPlaylistController(object): """ diff --git a/mopidy/core/library.py b/mopidy/core/library.py new file mode 100644 index 00000000..fc55aaeb --- /dev/null +++ b/mopidy/core/library.py @@ -0,0 +1,70 @@ +class LibraryController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`mopidy.backends.base.Backend` + :param provider: provider the controller should use + :type provider: instance of :class:`BaseLibraryProvider` + """ + + pykka_traversable = True + + def __init__(self, backend, provider): + self.backend = backend + self.provider = provider + + def find_exact(self, **query): + """ + Search the library for tracks where ``field`` is ``values``. + + Examples:: + + # Returns results matching 'a' + find_exact(any=['a']) + # Returns results matching artist 'xyz' + find_exact(artist=['xyz']) + # Returns results matching 'a' and 'b' and artist 'xyz' + find_exact(any=['a', 'b'], artist=['xyz']) + + :param query: one or more queries to search for + :type query: dict + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.find_exact(**query) + + def lookup(self, uri): + """ + Lookup track with given URI. Returns :class:`None` if not found. + + :param uri: track URI + :type uri: string + :rtype: :class:`mopidy.models.Track` or :class:`None` + """ + return self.provider.lookup(uri) + + 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 + """ + return self.provider.refresh(uri) + + def search(self, **query): + """ + Search the library for tracks where ``field`` contains ``values``. + + Examples:: + + # Returns results matching 'a' + search(any=['a']) + # Returns results matching artist 'xyz' + search(artist=['xyz']) + # Returns results matching 'a' and 'b' and artist 'xyz' + search(any=['a', 'b'], artist=['xyz']) + + :param query: one or more queries to search for + :type query: dict + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.search(**query) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py new file mode 100644 index 00000000..a0c3ef30 --- /dev/null +++ b/mopidy/core/playback.py @@ -0,0 +1,548 @@ +import logging +import random +import time + +from mopidy.listeners import BackendListener + + +logger = logging.getLogger('mopidy.backends.base') + + +def option_wrapper(name, default): + def get_option(self): + return getattr(self, name, default) + + def set_option(self, value): + if getattr(self, name, default) != value: + self._trigger_options_changed() + return setattr(self, name, value) + + return property(get_option, set_option) + + +class PlaybackController(object): + """ + :param backend: the backend + :type backend: :class:`mopidy.backends.base.Backend` + :param provider: provider the controller should use + :type provider: instance of :class:`BasePlaybackProvider` + """ + + # pylint: disable = R0902 + # Too many instance attributes + + pykka_traversable = True + + #: 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 = option_wrapper('_consume', False) + + #: The currently playing or selected track. + #: + #: A two-tuple of (CPID integer, :class:`mopidy.models.Track`) or + #: :class:`None`. + current_cp_track = None + + #: :class:`True` + #: Tracks are selected at random from the playlist. + #: :class:`False` + #: Tracks are played in the order of the playlist. + random = option_wrapper('_random', False) + + #: :class:`True` + #: The current playlist is played repeatedly. To repeat a single track, + #: select both :attr:`repeat` and :attr:`single`. + #: :class:`False` + #: The current playlist is played once. + repeat = option_wrapper('_repeat', False) + + #: :class:`True` + #: Playback is stopped after current song, unless in :attr:`repeat` + #: mode. + #: :class:`False` + #: Playback continues after current song. + single = option_wrapper('_single', False) + + def __init__(self, backend, provider): + self.backend = backend + self.provider = provider + self._state = self.STOPPED + self._shuffled = [] + self._first_shuffle = True + self.play_time_accumulated = 0 + self.play_time_started = None + + def _get_cpid(self, cp_track): + if cp_track is None: + return None + return cp_track.cpid + + def _get_track(self, cp_track): + if cp_track is None: + return None + return cp_track.track + + @property + def current_cpid(self): + """ + The CPID (current playlist ID) of the currently playing or selected + track. + + Read-only. Extracted from :attr:`current_cp_track` for convenience. + """ + return self._get_cpid(self.current_cp_track) + + @property + def current_track(self): + """ + The currently playing or selected :class:`mopidy.models.Track`. + + Read-only. Extracted from :attr:`current_cp_track` for convenience. + """ + return self._get_track(self.current_cp_track) + + @property + def current_playlist_position(self): + """ + The position of the current track in the current playlist. + + Read-only. + """ + if self.current_cp_track is None: + return None + try: + return self.backend.current_playlist.cp_tracks.index( + self.current_cp_track) + except ValueError: + return None + + @property + def track_at_eot(self): + """ + The track that will be played at the end of the current track. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_eot` for convenience. + """ + return self._get_track(self.cp_track_at_eot) + + @property + def cp_track_at_eot(self): + """ + The track that will be played at the end of the current track. + + Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + Not necessarily the same track as :attr:`cp_track_at_next`. + """ + # pylint: disable = R0911 + # Too many return statements + + cp_tracks = self.backend.current_playlist.cp_tracks + + if not cp_tracks: + return None + + if self.random and not self._shuffled: + if self.repeat or self._first_shuffle: + logger.debug('Shuffling tracks') + self._shuffled = cp_tracks + random.shuffle(self._shuffled) + self._first_shuffle = False + + if self.random and self._shuffled: + return self._shuffled[0] + + if self.current_cp_track is None: + return cp_tracks[0] + + if self.repeat and self.single: + return cp_tracks[self.current_playlist_position] + + if self.repeat and not self.single: + return cp_tracks[ + (self.current_playlist_position + 1) % len(cp_tracks)] + + try: + return cp_tracks[self.current_playlist_position + 1] + except IndexError: + return None + + @property + def track_at_next(self): + """ + The track that will be played if calling :meth:`next()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_next` for convenience. + """ + return self._get_track(self.cp_track_at_next) + + @property + def cp_track_at_next(self): + """ + The track that will be played if calling :meth:`next()`. + + Read-only. A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + For normal playback this is the next track in the playlist. If repeat + is enabled the next track can loop around the playlist. When random is + enabled this should be a random track, all tracks should be played once + before the list repeats. + """ + cp_tracks = self.backend.current_playlist.cp_tracks + + if not cp_tracks: + return None + + if self.random and not self._shuffled: + if self.repeat or self._first_shuffle: + logger.debug('Shuffling tracks') + self._shuffled = cp_tracks + random.shuffle(self._shuffled) + self._first_shuffle = False + + if self.random and self._shuffled: + return self._shuffled[0] + + if self.current_cp_track is None: + return cp_tracks[0] + + if self.repeat: + return cp_tracks[ + (self.current_playlist_position + 1) % len(cp_tracks)] + + try: + return cp_tracks[self.current_playlist_position + 1] + except IndexError: + return None + + @property + def track_at_previous(self): + """ + The track that will be played if calling :meth:`previous()`. + + Read-only. A :class:`mopidy.models.Track` extracted from + :attr:`cp_track_at_previous` for convenience. + """ + return self._get_track(self.cp_track_at_previous) + + @property + def cp_track_at_previous(self): + """ + The track that will be played if calling :meth:`previous()`. + + A two-tuple of (CPID integer, :class:`mopidy.models.Track`). + + For normal playback this is the previous track in the playlist. If + random and/or consume is enabled it should return the current track + instead. + """ + if self.repeat or self.consume or self.random: + return self.current_cp_track + + if self.current_playlist_position in (None, 0): + return None + + return self.backend.current_playlist.cp_tracks[ + self.current_playlist_position - 1] + + @property + def state(self): + """ + 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" ] + """ + 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) + + self._trigger_playback_state_changed() + + # FIXME play_time stuff assumes backend does not have a better way of + # handeling this stuff :/ + 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 = (self._current_wall_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 = self._current_wall_time + + def _play_time_pause(self): + time_since_started = self._current_wall_time - self.play_time_started + self.play_time_accumulated += time_since_started + + def _play_time_resume(self): + self.play_time_started = self._current_wall_time + + @property + def _current_wall_time(self): + return int(time.time() * 1000) + + @property + def volume(self): + return self.provider.get_volume() + + @volume.setter + def volume(self, volume): + self.provider.set_volume(volume) + + def change_track(self, cp_track, on_error_step=1): + """ + Change to the given track, keeping the current playback state. + + :param cp_track: track to change to + :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) + or :class:`None` + :param on_error_step: direction to step at play error, 1 for next + track (default), -1 for previous track + :type on_error_step: int, -1 or 1 + + """ + old_state = self.state + self.stop() + self.current_cp_track = cp_track + if old_state == self.PLAYING: + self.play(on_error_step=on_error_step) + elif old_state == self.PAUSED: + self.pause() + + def on_end_of_track(self): + """ + Tell the playback controller that end of track is reached. + """ + if self.state == self.STOPPED: + return + + original_cp_track = self.current_cp_track + + if self.cp_track_at_eot: + self._trigger_track_playback_ended() + self.play(self.cp_track_at_eot) + else: + self.stop(clear_current_track=True) + + if self.consume: + self.backend.current_playlist.remove(cpid=original_cp_track.cpid) + + def on_current_playlist_change(self): + """ + Tell the playback controller that the current playlist has changed. + + Used by :class:`mopidy.backends.base.CurrentPlaylistController`. + """ + self._first_shuffle = True + self._shuffled = [] + + if (not self.backend.current_playlist.cp_tracks or + self.current_cp_track not in + self.backend.current_playlist.cp_tracks): + self.stop(clear_current_track=True) + + 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. + """ + if self.cp_track_at_next: + self._trigger_track_playback_ended() + self.change_track(self.cp_track_at_next) + else: + self.stop(clear_current_track=True) + + def pause(self): + """Pause playback.""" + if self.provider.pause(): + self.state = self.PAUSED + self._trigger_track_playback_paused() + + def play(self, cp_track=None, on_error_step=1): + """ + Play the given track, or if the given track is :class:`None`, play the + currently active track. + + :param cp_track: track to play + :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) + or :class:`None` + :param on_error_step: direction to step at play error, 1 for next + track (default), -1 for previous track + :type on_error_step: int, -1 or 1 + """ + + if cp_track is not None: + assert cp_track in self.backend.current_playlist.cp_tracks + elif cp_track is None: + if self.state == self.PAUSED: + return self.resume() + elif self.current_cp_track is not None: + cp_track = self.current_cp_track + elif self.current_cp_track is None and on_error_step == 1: + cp_track = self.cp_track_at_next + elif self.current_cp_track is None and on_error_step == -1: + cp_track = self.cp_track_at_previous + + if cp_track is not None: + self.current_cp_track = cp_track + self.state = self.PLAYING + if not self.provider.play(cp_track.track): + # Track is not playable + if self.random and self._shuffled: + self._shuffled.remove(cp_track) + if on_error_step == 1: + self.next() + elif on_error_step == -1: + self.previous() + + if self.random and self.current_cp_track in self._shuffled: + self._shuffled.remove(self.current_cp_track) + + self._trigger_track_playback_started() + + 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. + """ + self._trigger_track_playback_ended() + self.change_track(self.cp_track_at_previous, on_error_step=-1) + + def resume(self): + """If paused, resume playing the current track.""" + if self.state == self.PAUSED and self.provider.resume(): + self.state = self.PLAYING + self._trigger_track_playback_resumed() + + 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.backend.current_playlist.tracks: + return False + + if self.state == self.STOPPED: + self.play() + elif self.state == self.PAUSED: + self.resume() + + if time_position < 0: + time_position = 0 + elif time_position > self.current_track.length: + self.next() + return True + + self.play_time_started = self._current_wall_time + self.play_time_accumulated = time_position + + success = self.provider.seek(time_position) + if success: + self._trigger_seeked() + return success + + def stop(self, clear_current_track=False): + """ + Stop playing. + + :param clear_current_track: whether to clear the current track _after_ + stopping + :type clear_current_track: boolean + """ + if self.state != self.STOPPED: + if self.provider.stop(): + self._trigger_track_playback_ended() + self.state = self.STOPPED + if clear_current_track: + self.current_cp_track = None + + def _trigger_track_playback_paused(self): + logger.debug(u'Triggering track playback paused event') + if self.current_track is None: + return + BackendListener.send('track_playback_paused', + track=self.current_track, + time_position=self.time_position) + + def _trigger_track_playback_resumed(self): + logger.debug(u'Triggering track playback resumed event') + if self.current_track is None: + return + BackendListener.send('track_playback_resumed', + track=self.current_track, + time_position=self.time_position) + + def _trigger_track_playback_started(self): + logger.debug(u'Triggering track playback started event') + if self.current_track is None: + return + BackendListener.send('track_playback_started', + track=self.current_track) + + def _trigger_track_playback_ended(self): + logger.debug(u'Triggering track playback ended event') + if self.current_track is None: + return + BackendListener.send('track_playback_ended', + track=self.current_track, + time_position=self.time_position) + + def _trigger_playback_state_changed(self): + logger.debug(u'Triggering playback state change event') + BackendListener.send('playback_state_changed') + + def _trigger_options_changed(self): + logger.debug(u'Triggering options changed event') + BackendListener.send('options_changed') + + def _trigger_seeked(self): + logger.debug(u'Triggering seeked event') + BackendListener.send('seeked') diff --git a/mopidy/core/stored_playlists.py b/mopidy/core/stored_playlists.py new file mode 100644 index 00000000..a29e34fc --- /dev/null +++ b/mopidy/core/stored_playlists.py @@ -0,0 +1,113 @@ +class StoredPlaylistsController(object): + """ + :param backend: backend the controller is a part of + :type backend: :class:`mopidy.backends.base.Backend` + :param provider: provider the controller should use + :type provider: instance of :class:`BaseStoredPlaylistsProvider` + """ + + pykka_traversable = True + + def __init__(self, backend, provider): + self.backend = backend + self.provider = provider + + @property + def playlists(self): + """ + Currently stored playlists. + + Read/write. List of :class:`mopidy.models.Playlist`. + """ + return self.provider.playlists + + @playlists.setter + def playlists(self, playlists): + self.provider.playlists = playlists + + def create(self, name): + """ + Create a new playlist. + + :param name: name of the new playlist + :type name: string + :rtype: :class:`mopidy.models.Playlist` + """ + return self.provider.create(name) + + def delete(self, playlist): + """ + Delete playlist. + + :param playlist: the playlist to delete + :type playlist: :class:`mopidy.models.Playlist` + """ + return self.provider.delete(playlist) + + def get(self, **criteria): + """ + Get playlist by given criterias from the set of stored playlists. + + Raises :exc:`LookupError` if a unique match is not found. + + Examples:: + + get(name='a') # Returns track with name 'a' + get(uri='xyz') # Returns track with URI 'xyz' + get(name='a', uri='xyz') # Returns track with name 'a' and URI + # 'xyz' + + :param criteria: one or more criteria to match by + :type criteria: dict + :rtype: :class:`mopidy.models.Playlist` + """ + matches = self.playlists + for (key, value) in criteria.iteritems(): + matches = filter(lambda p: getattr(p, key) == value, matches) + if len(matches) == 1: + return matches[0] + criteria_string = ', '.join( + ['%s=%s' % (k, v) for (k, v) in criteria.iteritems()]) + if len(matches) == 0: + raise LookupError('"%s" match no playlists' % criteria_string) + else: + raise LookupError('"%s" match multiple playlists' + % criteria_string) + + 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` + """ + return self.provider.lookup(uri) + + def refresh(self): + """ + Refresh the stored playlists in + :attr:`mopidy.backends.base.StoredPlaylistsController.playlists`. + """ + return self.provider.refresh() + + 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 + """ + return self.provider.rename(playlist, new_name) + + def save(self, playlist): + """ + Save the playlist to the set of stored playlists. + + :param playlist: the playlist + :type playlist: :class:`mopidy.models.Playlist` + """ + return self.provider.save(playlist) diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 4cf33266..e6bb6478 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,4 +1,4 @@ -from mopidy.backends.base import PlaybackController +from mopidy import core from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @@ -105,10 +105,10 @@ def pause(context, state=None): """ if state is None: if (context.backend.playback.state.get() == - PlaybackController.PLAYING): + core.PlaybackController.PLAYING): context.backend.playback.pause() elif (context.backend.playback.state.get() == - PlaybackController.PAUSED): + core.PlaybackController.PAUSED): context.backend.playback.resume() elif int(state): context.backend.playback.pause() @@ -185,9 +185,11 @@ def playpos(context, songpos): raise MpdArgError(u'Bad song index', command=u'play') def _play_minus_one(context): - if (context.backend.playback.state.get() == PlaybackController.PLAYING): + if (context.backend.playback.state.get() == + core.PlaybackController.PLAYING): return # Nothing to do - elif (context.backend.playback.state.get() == PlaybackController.PAUSED): + elif (context.backend.playback.state.get() == + core.PlaybackController.PAUSED): return context.backend.playback.resume().get() elif context.backend.playback.current_cp_track.get() is not None: cp_track = context.backend.playback.current_cp_track.get() diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 4a9ad9a1..279978aa 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,6 +1,6 @@ import pykka.future -from mopidy.backends.base import PlaybackController +from mopidy import core from mopidy.frontends.mpd.exceptions import MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.translator import track_to_mpd_format @@ -194,8 +194,8 @@ def status(context): if futures['playback.current_cp_track'].get() is not None: result.append(('song', _status_songpos(futures))) result.append(('songid', _status_songid(futures))) - if futures['playback.state'].get() in (PlaybackController.PLAYING, - PlaybackController.PAUSED): + if futures['playback.state'].get() in (core.PlaybackController.PLAYING, + core.PlaybackController.PAUSED): result.append(('time', _status_time(futures))) result.append(('elapsed', _status_time_elapsed(futures))) result.append(('bitrate', _status_bitrate(futures))) @@ -239,11 +239,11 @@ def _status_songpos(futures): def _status_state(futures): state = futures['playback.state'].get() - if state == PlaybackController.PLAYING: + if state == core.PlaybackController.PLAYING: return u'play' - elif state == PlaybackController.STOPPED: + elif state == core.PlaybackController.STOPPED: return u'stop' - elif state == PlaybackController.PAUSED: + elif state == core.PlaybackController.PAUSED: return u'pause' def _status_time(futures): diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 6815c0d2..bcd3de5c 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -14,9 +14,8 @@ except ImportError as import_error: from pykka.registry import ActorRegistry -from mopidy import settings +from mopidy import core, settings from mopidy.backends.base import Backend -from mopidy.backends.base.playback import PlaybackController from mopidy.utils.process import exit_process # Must be done before dbus.SessionBus() is called @@ -198,11 +197,11 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE) return state = self.backend.playback.state.get() - if state == PlaybackController.PLAYING: + if state == core.PlaybackController.PLAYING: self.backend.playback.pause().get() - elif state == PlaybackController.PAUSED: + elif state == core.PlaybackController.PAUSED: self.backend.playback.resume().get() - elif state == PlaybackController.STOPPED: + elif state == core.PlaybackController.STOPPED: self.backend.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -220,7 +219,7 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.Play not allowed', PLAYER_IFACE) return state = self.backend.playback.state.get() - if state == PlaybackController.PAUSED: + if state == core.PlaybackController.PAUSED: self.backend.playback.resume().get() else: self.backend.playback.play().get() @@ -287,11 +286,11 @@ class MprisObject(dbus.service.Object): def get_PlaybackStatus(self): state = self.backend.playback.state.get() - if state == PlaybackController.PLAYING: + if state == core.PlaybackController.PLAYING: return 'Playing' - elif state == PlaybackController.PAUSED: + elif state == core.PlaybackController.PAUSED: return 'Paused' - elif state == PlaybackController.STOPPED: + elif state == core.PlaybackController.STOPPED: return 'Stopped' def get_LoopStatus(self): diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 87c9bbb8..514c1599 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -1,12 +1,12 @@ -from mopidy.backends import base as backend +from mopidy import core from mopidy.models import Track from tests import unittest from tests.frontends.mpd import protocol -PAUSED = backend.PlaybackController.PAUSED -PLAYING = backend.PlaybackController.PLAYING -STOPPED = backend.PlaybackController.STOPPED +PAUSED = core.PlaybackController.PAUSED +PLAYING = core.PlaybackController.PLAYING +STOPPED = core.PlaybackController.STOPPED class PlaybackOptionsHandlerTest(protocol.BaseTestCase): diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 3701faaf..8fd8895d 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,13 +1,14 @@ -from mopidy.backends import dummy as backend +from mopidy import core +from mopidy.backends import dummy from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status from mopidy.models import Track from tests import unittest -PAUSED = backend.PlaybackController.PAUSED -PLAYING = backend.PlaybackController.PLAYING -STOPPED = backend.PlaybackController.STOPPED +PAUSED = core.PlaybackController.PAUSED +PLAYING = core.PlaybackController.PLAYING +STOPPED = core.PlaybackController.STOPPED # FIXME migrate to using protocol.BaseTestCase instead of status.stats # directly? @@ -15,7 +16,7 @@ STOPPED = backend.PlaybackController.STOPPED class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.backend = backend.DummyBackend.start().proxy() + self.backend = dummy.DummyBackend.start().proxy() self.dispatcher = dispatcher.MpdDispatcher() self.context = self.dispatcher.context diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index b7ad1b60..48be504f 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -2,9 +2,8 @@ import sys import mock -from mopidy import OptionalDependencyError +from mopidy import core, OptionalDependencyError from mopidy.backends.dummy import DummyBackend -from mopidy.backends.base.playback import PlaybackController from mopidy.models import Album, Artist, Track try: @@ -14,9 +13,9 @@ except OptionalDependencyError: from tests import unittest -PLAYING = PlaybackController.PLAYING -PAUSED = PlaybackController.PAUSED -STOPPED = PlaybackController.STOPPED +PLAYING = core.PlaybackController.PLAYING +PAUSED = core.PlaybackController.PAUSED +STOPPED = core.PlaybackController.STOPPED @unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux')