diff --git a/bin/mopidy b/bin/mopidy index 0472518e..aabf21d3 100755 --- a/bin/mopidy +++ b/bin/mopidy @@ -1,5 +1,5 @@ #! /usr/bin/env python if __name__ == '__main__': - from mopidy.__main__ import main + from mopidy.core import main main() diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index fbc7baee..9ea3533f 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -74,11 +74,11 @@ Running tests To run tests, you need a couple of dependencies. They can be installed through Debian/Ubuntu package management:: - sudo aptitude install python-coverage python-nose + sudo aptitude install python-coverage python-mock python-nose Or, they can be installed using ``pip``:: - sudo pip install -r requirements-tests.txt + sudo pip install -r requirements/tests.txt Then, to run all tests, go to the project directory and run:: diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 56f0015b..d1fbd0f6 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -25,6 +25,8 @@ Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 +- `Pykka `_ >= 0.12 + - GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. - Mixer dependencies: The default mixer does not require any additional diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 873ee182..e9ced3ae 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -9,7 +9,7 @@ VERSION = (0, 4, 0) def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) if process.wait() != 0: - raise Exception('Execution of "git describe" failed') + raise EnvironmentError('Execution of "git describe" failed') version = process.stdout.read().strip() if version.startswith('v'): version = version[1:] @@ -21,7 +21,7 @@ def get_plain_version(): def get_version(): try: return get_git_version() - except Exception: + except EnvironmentError: return get_plain_version() class MopidyException(Exception): diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 20e78f5a..169c2754 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -1,17 +1,10 @@ -import os -import sys - # Add ../ to the path so we can run Mopidy from a Git checkout without # installing it on the system. +import os +import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -from mopidy.core import CoreProcess - -def main(): - # Explictly call run() instead of start(), since we don't need to start - # another process. - CoreProcess().run() - if __name__ == '__main__': + from mopidy.core import main main() diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 096a433f..038e2d7b 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -1,12 +1,4 @@ -from copy import copy import logging -import random -import time - -from mopidy import settings -from mopidy.frontends.mpd import translator -from mopidy.models import Playlist -from mopidy.utils import get_class from .current_playlist import CurrentPlaylistController from .library import LibraryController, BaseLibraryProvider @@ -17,30 +9,6 @@ from .stored_playlists import (StoredPlaylistsController, logger = logging.getLogger('mopidy.backends.base') class Backend(object): - """ - :param core_queue: a queue for sending messages to - :class:`mopidy.process.CoreProcess` - :type core_queue: :class:`multiprocessing.Queue` - :param output: the audio output - :type output: :class:`mopidy.outputs.gstreamer.GStreamerOutput` or similar - :param mixer_class: either a mixer class, or :class:`None` to use the mixer - defined in settings - :type mixer_class: a subclass of :class:`mopidy.mixers.BaseMixer` or - :class:`None` - """ - - def __init__(self, core_queue=None, output=None, mixer_class=None): - self.core_queue = core_queue - self.output = output - if mixer_class is None: - mixer_class = get_class(settings.MIXER) - self.mixer = mixer_class(self) - - #: A :class:`multiprocessing.Queue` which can be used by e.g. library - #: callbacks executing in other threads to send messages to the core - #: thread, so that action may be taken in the correct thread. - core_queue = None - #: The current playlist controller. An instance of #: :class:`mopidy.backends.base.CurrentPlaylistController`. current_playlist = None @@ -49,9 +17,6 @@ class Backend(object): # :class:`mopidy.backends.base.LibraryController`. library = None - #: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`. - mixer = None - #: The playback controller. An instance of #: :class:`mopidy.backends.base.PlaybackController`. playback = None @@ -62,24 +27,3 @@ class Backend(object): #: List of URI prefixes this backend can handle. uri_handlers = [] - - def destroy(self): - """ - Call destroy on all sub-components in backend so that they can cleanup - after themselves. - """ - - if self.current_playlist: - self.current_playlist.destroy() - - if self.library: - self.library.destroy() - - if self.mixer: - self.mixer.destroy() - - if self.playback: - self.playback.destroy() - - if self.stored_playlists: - self.stored_playlists.destroy() diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index fe7d1de9..ffdce176 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -2,8 +2,6 @@ from copy import copy import logging import random -from mopidy.frontends.mpd import translator - logger = logging.getLogger('mopidy.backends.base') class CurrentPlaylistController(object): @@ -12,6 +10,8 @@ class CurrentPlaylistController(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend self._cp_tracks = [] @@ -197,8 +197,3 @@ class CurrentPlaylistController(object): random.shuffle(shuffled) self._cp_tracks = before + shuffled + after self.version += 1 - - def mpd_format(self, *args, **kwargs): - """Not a part of the generic backend API.""" - kwargs['cpids'] = [ct[0] for ct in self._cp_tracks] - return translator.tracks_to_mpd_format(self.tracks, *args, **kwargs) diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index fd018b5f..a30ed412 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -10,6 +10,8 @@ class LibraryController(object): :type provider: instance of :class:`BaseLibraryProvider` """ + pykka_traversable = True + def __init__(self, backend, provider): self.backend = backend self.provider = provider @@ -82,6 +84,8 @@ class BaseLibraryProvider(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 8a3eeee5..2e690b4a 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -2,6 +2,10 @@ import logging import random import time +from pykka.registry import ActorRegistry + +from mopidy.frontends.base import BaseFrontend + logger = logging.getLogger('mopidy.backends.base') class PlaybackController(object): @@ -15,6 +19,8 @@ class PlaybackController(object): # pylint: disable = R0902 # Too many instance attributes + pykka_traversable = True + #: Constant representing the paused state. PAUSED = u'paused' @@ -62,8 +68,8 @@ class PlaybackController(object): self._state = self.STOPPED self._shuffled = [] self._first_shuffle = True - self._play_time_accumulated = 0 - self._play_time_started = None + self.play_time_accumulated = 0 + self.play_time_started = None def destroy(self): """ @@ -269,7 +275,7 @@ class PlaybackController(object): def state(self, new_state): (old_state, self._state) = (self.state, new_state) logger.debug(u'Changing state: %s -> %s', old_state, new_state) - # FIXME _play_time stuff assumes backend does not have a better way of + # 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): @@ -284,23 +290,23 @@ class PlaybackController(object): """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 + self.play_time_started) + return self.play_time_accumulated + time_since_started elif self.state == self.PAUSED: - return self._play_time_accumulated + 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 + 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 + 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 + self.play_time_started = self._current_wall_time @property def _current_wall_time(self): @@ -433,8 +439,8 @@ class PlaybackController(object): self.next() return True - self._play_time_started = self._current_wall_time - self._play_time_accumulated = time_position + self.play_time_started = self._current_wall_time + self.play_time_accumulated = time_position return self.provider.seek(time_position) @@ -461,9 +467,11 @@ class PlaybackController(object): For internal use only. Should be called by the backend directly after a track has started playing. """ - if self.current_track is not None: - self.backend.core_queue.put({ - 'to': 'frontend', + if self.current_track is None: + return + frontend_refs = ActorRegistry.get_by_class(BaseFrontend) + for frontend_ref in frontend_refs: + frontend_ref.send_one_way({ 'command': 'started_playing', 'track': self.current_track, }) @@ -476,9 +484,11 @@ class PlaybackController(object): is stopped playing, e.g. at the next, previous, and stop actions and at end-of-track. """ - if self.current_track is not None: - self.backend.core_queue.put({ - 'to': 'frontend', + if self.current_track is None: + return + frontend_refs = ActorRegistry.get_by_class(BaseFrontend) + for frontend_ref in frontend_refs: + frontend_ref.send_one_way({ 'command': 'stopped_playing', 'track': self.current_track, 'stop_position': self.time_position, @@ -491,6 +501,8 @@ class BasePlaybackProvider(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 6578c046..aca78a8c 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -11,6 +11,8 @@ class StoredPlaylistsController(object): :type provider: instance of :class:`BaseStoredPlaylistsProvider` """ + pykka_traversable = True + def __init__(self, backend, provider): self.backend = backend self.provider = provider @@ -125,6 +127,8 @@ class BaseStoredPlaylistsProvider(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend self._playlists = [] diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 9c6885bc..90c87dac 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,3 +1,5 @@ +from pykka.actor import ThreadingActor + from mopidy.backends.base import (Backend, CurrentPlaylistController, PlaybackController, BasePlaybackProvider, LibraryController, BaseLibraryProvider, StoredPlaylistsController, @@ -5,15 +7,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, from mopidy.models import Playlist -class DummyQueue(object): - def __init__(self): - self.received_messages = [] - - def put(self, message): - self.received_messages.append(message) - - -class DummyBackend(Backend): +class DummyBackend(ThreadingActor, Backend): """ A backend which implements the backend API in the simplest way possible. Used in tests of the frontends. @@ -24,8 +18,6 @@ class DummyBackend(Backend): def __init__(self, *args, **kwargs): super(DummyBackend, self).__init__(*args, **kwargs) - self.core_queue = DummyQueue() - self.current_playlist = CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) @@ -46,13 +38,13 @@ class DummyBackend(Backend): class DummyLibraryProvider(BaseLibraryProvider): def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) - self._library = [] + self.dummy_library = [] def find_exact(self, **query): return Playlist() def lookup(self, uri): - matches = filter(lambda t: uri == t.uri, self._library) + matches = filter(lambda t: uri == t.uri, self.dummy_library) if matches: return matches[0] diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e3e1d5dc..2fa96dab 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -1,22 +1,24 @@ import glob import logging -import multiprocessing import os import shutil +from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry + from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, BaseLibraryProvider, PlaybackController, BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album -from mopidy.utils.process import pickle_connection +from mopidy.outputs.base import BaseOutput from .translator import parse_m3u, parse_mpd_tag_cache logger = logging.getLogger(u'mopidy.backends.local') -class LocalBackend(Backend): +class LocalBackend(ThreadingActor, Backend): """ A backend for playing music from a local music archive. @@ -48,6 +50,13 @@ class LocalBackend(Backend): self.uri_handlers = [u'file://'] + self.output = None + + def on_start(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() + class LocalPlaybackController(PlaybackController): def __init__(self, *args, **kwargs): @@ -58,24 +67,24 @@ class LocalPlaybackController(PlaybackController): @property def time_position(self): - return self.backend.output.get_position() + return self.backend.output.get_position().get() class LocalPlaybackProvider(BasePlaybackProvider): def pause(self): - return self.backend.output.set_state('PAUSED') + return self.backend.output.set_state('PAUSED').get() def play(self, track): - return self.backend.output.play_uri(track.uri) + return self.backend.output.play_uri(track.uri).get() def resume(self): - return self.backend.output.set_state('PLAYING') + return self.backend.output.set_state('PLAYING').get() def seek(self, time_position): - return self.backend.output.set_position(time_position) + return self.backend.output.set_position(time_position).get() def stop(self): - return self.backend.output.set_state('READY') + return self.backend.output.set_state('READY').get() class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index d36f6250..1ac5f0be 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -1,14 +1,18 @@ 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.outputs.base import BaseOutput logger = logging.getLogger('mopidy.backends.spotify') ENCODING = 'utf-8' -class SpotifyBackend(Backend): +class SpotifyBackend(ThreadingActor, Backend): """ A backend for playing music from the `Spotify `_ music streaming service. The backend uses the official `libspotify @@ -59,6 +63,14 @@ class SpotifyBackend(Backend): self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.output = None + self.spotify = None + + def on_start(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() + self.spotify = self._connect() def _connect(self): @@ -67,8 +79,6 @@ class SpotifyBackend(Backend): logger.info(u'Mopidy uses SPOTIFY(R) CORE') logger.debug(u'Connecting to Spotify') spotify = SpotifySessionManager( - settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, - core_queue=self.core_queue, - output=self.output) + settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD) spotify.start() return spotify diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 5e2f66ae..40d4a099 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -1,5 +1,5 @@ import logging -import multiprocessing +import Queue from spotify import Link, SpotifyError @@ -54,8 +54,9 @@ class SpotifyLibraryProvider(BaseLibraryProvider): spotify_query.append(u'%s:"%s"' % (field, value)) spotify_query = u' '.join(spotify_query) logger.debug(u'Spotify search query: %s' % spotify_query) - my_end, other_end = multiprocessing.Pipe() - self.backend.spotify.search(spotify_query.encode(ENCODING), other_end) - my_end.poll(None) - playlist = my_end.recv() - return playlist + queue = Queue.Queue() + self.backend.spotify.search(spotify_query.encode(ENCODING), queue) + try: + return queue.get(timeout=3) # XXX What is an reasonable timeout? + except Queue.Empty: + return Playlist(tracks=[]) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 7ed12ada..e92fe89e 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -2,11 +2,15 @@ import logging import os import threading -import spotify.manager +from spotify.manager import SpotifySessionManager as PyspotifySessionManager + +from pykka.registry import ActorRegistry from mopidy import get_version, settings +from mopidy.backends.base import Backend from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist +from mopidy.outputs.base import BaseOutput from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.spotify.session_manager') @@ -14,24 +18,36 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager') # pylint: disable = R0901 # SpotifySessionManager: Too many ancestors (9/7) -class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): +class SpotifySessionManager(BaseThread, PyspotifySessionManager): cache_location = settings.SPOTIFY_CACHE_PATH settings_location = settings.SPOTIFY_CACHE_PATH appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() - def __init__(self, username, password, core_queue, output): - spotify.manager.SpotifySessionManager.__init__( - self, username, password) - BaseThread.__init__(self, core_queue) + def __init__(self, username, password): + PyspotifySessionManager.__init__(self, username, password) + BaseThread.__init__(self) self.name = 'SpotifySMThread' - self.output = output + + self.output = None + self.backend = None + self.connected = threading.Event() self.session = None def run_inside_try(self): + self.setup() self.connect() + def setup(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() + + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + self.backend = backend_refs[0].proxy() + def logged_in(self, session, error): """Callback used by pyspotify""" if error: @@ -91,7 +107,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): def play_token_lost(self, session): """Callback used by pyspotify""" logger.debug(u'Play token lost') - self.core_queue.put({'command': 'stop_playback'}) + self.backend.playback.pause() def log_message(self, session, data): """Callback used by pyspotify""" @@ -110,19 +126,16 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): playlists.append( SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) playlists = filter(None, playlists) - self.core_queue.put({ - 'command': 'set_stored_playlists', - 'playlists': playlists, - }) + self.backend.stored_playlists.playlists = playlists logger.debug(u'Refreshed %d stored playlist(s)', len(playlists)) - def search(self, query, connection): + def search(self, query, queue): """Search method used by Mopidy backend""" def callback(results, userdata=None): # TODO Include results from results.albums(), etc. too playlist = Playlist(tracks=[ SpotifyTranslator.to_mopidy_track(t) for t in results.tracks()]) - connection.send(playlist) + queue.put(playlist) self.connected.wait() self.session.search(query, callback) diff --git a/mopidy/core.py b/mopidy/core.py index 56febe5b..a1c6b361 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,114 +1,74 @@ import logging -import multiprocessing import optparse -import sys +import time + +from pykka.registry import ActorRegistry from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file -from mopidy.utils.process import BaseThread, GObjectEventThread +from mopidy.utils.process import GObjectEventThread from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') -class CoreProcess(BaseThread): - def __init__(self): - self.core_queue = multiprocessing.Queue() - super(CoreProcess, self).__init__(self.core_queue) - self.name = 'CoreProcess' - self.options = self.parse_options() - self.gobject_loop = None - self.output = None - self.backend = None - self.frontends = [] +def main(): + options = parse_options() + setup_logging(options.verbosity_level, options.save_debug_log) + setup_settings() + setup_gobject_loop() + setup_output() + setup_mixer() + setup_backend() + setup_frontends() + try: + time.sleep(10000*24*60*60) + except KeyboardInterrupt: + logger.info(u'Exiting...') + ActorRegistry.stop_all() - def parse_options(self): - parser = optparse.OptionParser(version='Mopidy %s' % get_version()) - parser.add_option('-q', '--quiet', - action='store_const', const=0, dest='verbosity_level', - help='less output (warning level)') - parser.add_option('-v', '--verbose', - action='store_const', const=2, dest='verbosity_level', - help='more output (debug level)') - parser.add_option('--save-debug-log', - action='store_true', dest='save_debug_log', - help='save debug log to "./mopidy.log"') - parser.add_option('--list-settings', - action='callback', callback=list_settings_optparse_callback, - help='list current settings') - return parser.parse_args()[0] +def parse_options(): + parser = optparse.OptionParser(version='Mopidy %s' % get_version()) + parser.add_option('-q', '--quiet', + action='store_const', const=0, dest='verbosity_level', + help='less output (warning level)') + parser.add_option('-v', '--verbose', + action='store_const', const=2, dest='verbosity_level', + help='more output (debug level)') + parser.add_option('--save-debug-log', + action='store_true', dest='save_debug_log', + help='save debug log to "./mopidy.log"') + parser.add_option('--list-settings', + action='callback', callback=list_settings_optparse_callback, + help='list current settings') + return parser.parse_args()[0] - def run_inside_try(self): - self.setup() - while True: - message = self.core_queue.get() - self.process_message(message) +def setup_settings(): + get_or_create_folder('~/.mopidy/') + get_or_create_file('~/.mopidy/settings.py') + settings.validate() - def setup(self): - self.setup_logging() - self.setup_settings() - self.gobject_loop = self.setup_gobject_loop(self.core_queue) - self.output = self.setup_output(self.core_queue) - self.backend = self.setup_backend(self.core_queue, self.output) - self.frontends = self.setup_frontends(self.core_queue, self.backend) +def setup_gobject_loop(): + gobject_loop = GObjectEventThread() + gobject_loop.start() + return gobject_loop - def setup_logging(self): - setup_logging(self.options.verbosity_level, - self.options.save_debug_log) - logger.info(u'-- Starting Mopidy %s --', get_version()) +def setup_output(): + return get_class(settings.OUTPUT).start().proxy() - def setup_settings(self): - get_or_create_folder('~/.mopidy/') - get_or_create_file('~/.mopidy/settings.py') - settings.validate() +def setup_mixer(): + return get_class(settings.MIXER).start().proxy() - def setup_gobject_loop(self, core_queue): - gobject_loop = GObjectEventThread(core_queue) - gobject_loop.start() - return gobject_loop +def setup_backend(): + return get_class(settings.BACKENDS[0]).start().proxy() - def setup_output(self, core_queue): - output = get_class(settings.OUTPUT)(core_queue) - output.start() - return output - - def setup_backend(self, core_queue, output): - return get_class(settings.BACKENDS[0])(core_queue, output) - - def setup_frontends(self, core_queue, backend): - frontends = [] - for frontend_class_name in settings.FRONTENDS: - try: - frontend = get_class(frontend_class_name)(core_queue, backend) - frontend.start() - frontends.append(frontend) - except OptionalDependencyError as e: - logger.info(u'Disabled: %s (%s)', frontend_class_name, e) - return frontends - - def process_message(self, message): - if message.get('to') == 'core': - self.process_message_to_core(message) - elif message.get('to') == 'output': - self.output.process_message(message) - elif message.get('to') == 'frontend': - for frontend in self.frontends: - frontend.process_message(message) - elif message['command'] == 'end_of_track': - self.backend.playback.on_end_of_track() - elif message['command'] == 'stop_playback': - self.backend.playback.stop() - elif message['command'] == 'set_stored_playlists': - self.backend.stored_playlists.playlists = message['playlists'] - else: - logger.warning(u'Cannot handle message: %s', message) - - def process_message_to_core(self, message): - assert message['to'] == 'core', u'Message recipient must be "core".' - if message['command'] == 'exit': - if message['reason'] is not None: - logger.info(u'Exiting (%s)', message['reason']) - sys.exit(message['status']) - else: - logger.warning(u'Cannot handle message: %s', message) +def setup_frontends(): + frontends = [] + for frontend_class_name in settings.FRONTENDS: + try: + frontend = get_class(frontend_class_name).start().proxy() + frontends.append(frontend) + except OptionalDependencyError as e: + logger.info(u'Disabled: %s (%s)', frontend_class_name, e) + return frontends diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py index bf1c9bda..811644b1 100644 --- a/mopidy/frontends/base.py +++ b/mopidy/frontends/base.py @@ -1,40 +1,5 @@ class BaseFrontend(object): """ Base class for frontends. - - :param core_queue: queue for messaging the core - :type core_queue: :class:`multiprocessing.Queue` - :param backend: the backend - :type backend: :class:`mopidy.backends.base.Backend` """ - - def __init__(self, core_queue, backend): - self.core_queue = core_queue - self.backend = backend - - def start(self): - """ - Start the frontend. - - *MAY be implemented by subclass.* - """ - pass - - def destroy(self): - """ - Destroy the frontend. - - *MAY be implemented by subclass.* - """ - pass - - def process_message(self, message): - """ - Process messages for the frontend. - - *MUST be implemented by subclass.* - - :param message: the message - :type message: dict - """ - raise NotImplementedError + pass diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index d2c9af88..04716c61 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -1,5 +1,4 @@ import logging -import multiprocessing import time try: @@ -8,16 +7,17 @@ except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) +from pykka.actor import ThreadingActor + from mopidy import settings, SettingsError from mopidy.frontends.base import BaseFrontend -from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.lastfm') API_KEY = '2236babefa8ebb3d93ea467560d00d04' API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' -class LastfmFrontend(BaseFrontend): +class LastfmFrontend(ThreadingActor, BaseFrontend): """ Frontend which scrobbles the music you play to your `Last.fm `_ profile. @@ -36,38 +36,11 @@ class LastfmFrontend(BaseFrontend): - :attr:`mopidy.settings.LASTFM_PASSWORD` """ - def __init__(self, *args, **kwargs): - super(LastfmFrontend, self).__init__(*args, **kwargs) - (self.connection, other_end) = multiprocessing.Pipe() - self.thread = LastfmFrontendThread(self.core_queue, other_end) - - def start(self): - self.thread.start() - - def destroy(self): - self.thread.destroy() - - def process_message(self, message): - if self.thread.is_alive(): - self.connection.send(message) - - -class LastfmFrontendThread(BaseThread): - def __init__(self, core_queue, connection): - super(LastfmFrontendThread, self).__init__(core_queue) - self.name = u'LastfmFrontendThread' - self.connection = connection + def __init__(self): self.lastfm = None self.last_start_time = None - def run_inside_try(self): - self.setup() - while self.lastfm is not None: - self.connection.poll(None) - message = self.connection.recv() - self.process_message(message) - - def setup(self): + def on_start(self): try: username = settings.LASTFM_USERNAME password_hash = pylast.md5(settings.LASTFM_PASSWORD) @@ -78,17 +51,19 @@ class LastfmFrontendThread(BaseThread): except SettingsError as e: logger.info(u'Last.fm scrobbler not started') logger.debug(u'Last.fm settings error: %s', e) + self.stop() except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: logger.error(u'Error during Last.fm setup: %s', e) + self.stop() - def process_message(self, message): - if message['command'] == 'started_playing': + def on_receive(self, message): + if message.get('command') == 'started_playing': self.started_playing(message['track']) - elif message['command'] == 'stopped_playing': + elif message.get('command') == 'stopped_playing': self.stopped_playing(message['track'], message['stop_position']) else: - pass # Ignore commands for other frontends + pass # Ignore any other messages def started_playing(self, track): artists = ', '.join([a.name for a in track.artists]) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 2f87088c..24c21c38 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,13 +1,15 @@ +import asyncore import logging +from pykka.actor import ThreadingActor + from mopidy.frontends.base import BaseFrontend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.thread import MpdThread -from mopidy.utils.process import unpickle_connection +from mopidy.frontends.mpd.server import MpdServer +from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.mpd') -class MpdFrontend(BaseFrontend): +class MpdFrontend(ThreadingActor, BaseFrontend): """ The MPD frontend. @@ -18,32 +20,24 @@ class MpdFrontend(BaseFrontend): - :attr:`mopidy.settings.MPD_SERVER_PORT` """ - def __init__(self, *args, **kwargs): - super(MpdFrontend, self).__init__(*args, **kwargs) - self.thread = None - self.dispatcher = MpdDispatcher(self.backend) + def __init__(self): + self._thread = None - def start(self): - """Starts the MPD server.""" - self.thread = MpdThread(self.core_queue) - self.thread.start() + def on_start(self): + self._thread = MpdThread() + self._thread.start() - def destroy(self): - """Destroys the MPD server.""" - self.thread.destroy() + def on_receive(self, message): + pass # Ignore any messages - def process_message(self, message): - """ - Processes messages with the MPD frontend as destination. - :param message: the message - :type message: dict - """ - assert message['to'] == 'frontend', \ - u'Message recipient must be "frontend".' - if message['command'] == 'mpd_request': - response = self.dispatcher.handle_request(message['request']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - else: - pass # Ignore messages for other frontends +class MpdThread(BaseThread): + def __init__(self): + super(MpdThread, self).__init__() + self.name = u'MpdThread' + + def run_inside_try(self): + logger.debug(u'Starting MPD server thread') + server = MpdServer() + server.start() + asyncore.loop() diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index ab5f2e8c..f5c30b23 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -1,5 +1,8 @@ import re +from pykka.registry import ActorRegistry + +from mopidy.backends.base import Backend from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, MpdUnknownCommand) from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers @@ -10,15 +13,27 @@ from mopidy.frontends.mpd.protocol import (audio_output, command_list, connection, current_playlist, empty, music_db, playback, reflection, status, stickers, stored_playlists) # pylint: enable = W0611 +from mopidy.mixers.base import BaseMixer from mopidy.utils import flatten class MpdDispatcher(object): """ - Dispatches MPD requests to the correct handler. + The MPD session feeds the MPD dispatcher with requests. The dispatcher + finds the correct handler, processes the request and sends the response + back to the MPD session. """ - def __init__(self, backend=None): - self.backend = backend + # XXX Consider merging MpdDispatcher into MpdSession + + def __init__(self): + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + self.backend = backend_refs[0].proxy() + + mixer_refs = ActorRegistry.get_by_class(BaseMixer) + assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' + self.mixer = mixer_refs[0].proxy() + self.command_list = False self.command_list_ok = False diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py index d25fc118..98c1d645 100644 --- a/mopidy/frontends/mpd/protocol/audio_output.py +++ b/mopidy/frontends/mpd/protocol/audio_output.py @@ -34,6 +34,6 @@ def outputs(frontend): """ return [ ('outputid', 0), - ('outputname', frontend.backend.__class__.__name__), + ('outputname', None), ('outputenabled', 1), ] diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 2f0a9f8f..8ef5e026 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -1,6 +1,7 @@ -from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.translator import tracks_to_mpd_format @handle_pattern(r'^add "(?P[^"]*)"$') def add(frontend, uri): @@ -18,9 +19,9 @@ def add(frontend, uri): """ if not uri: return - for handler_prefix in frontend.backend.uri_handlers: + for handler_prefix in frontend.backend.uri_handlers.get(): if uri.startswith(handler_prefix): - track = frontend.backend.library.lookup(uri) + track = frontend.backend.library.lookup(uri).get() if track is not None: frontend.backend.current_playlist.add(track) return @@ -50,13 +51,14 @@ def addid(frontend, uri, songpos=None): raise MpdNoExistError(u'No such song', command=u'addid') if songpos is not None: songpos = int(songpos) - track = frontend.backend.library.lookup(uri) + track = frontend.backend.library.lookup(uri).get() if track is None: raise MpdNoExistError(u'No such song', command=u'addid') - if songpos and songpos > len(frontend.backend.current_playlist.tracks): + if songpos and songpos > len( + frontend.backend.current_playlist.tracks.get()): raise MpdArgError(u'Bad song index', command=u'addid') cp_track = frontend.backend.current_playlist.add(track, - at_position=songpos) + at_position=songpos).get() return ('Id', cp_track[0]) @handle_pattern(r'^delete "(?P\d+):(?P\d+)*"$') @@ -72,8 +74,8 @@ def delete_range(frontend, start, end=None): if end is not None: end = int(end) else: - end = len(frontend.backend.current_playlist.tracks) - cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end] + end = len(frontend.backend.current_playlist.tracks.get()) + cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end] if not cp_tracks: raise MpdArgError(u'Bad song index', command=u'delete') for (cpid, _) in cp_tracks: @@ -84,7 +86,7 @@ def delete_songpos(frontend, songpos): """See :meth:`delete_range`""" try: songpos = int(songpos) - (cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos] + (cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos] frontend.backend.current_playlist.remove(cpid=cpid) except IndexError: raise MpdArgError(u'Bad song index', command=u'delete') @@ -100,9 +102,9 @@ def deleteid(frontend, cpid): """ try: cpid = int(cpid) - if frontend.backend.playback.current_cpid == cpid: + if frontend.backend.playback.current_cpid.get() == cpid: frontend.backend.playback.next() - return frontend.backend.current_playlist.remove(cpid=cpid) + return frontend.backend.current_playlist.remove(cpid=cpid).get() except LookupError: raise MpdNoExistError(u'No such song', command=u'deleteid') @@ -128,7 +130,7 @@ def move_range(frontend, start, to, end=None): ``TO`` in the playlist. """ if end is None: - end = len(frontend.backend.current_playlist.tracks) + end = len(frontend.backend.current_playlist.tracks.get()) start = int(start) end = int(end) to = int(to) @@ -154,8 +156,9 @@ def moveid(frontend, cpid, to): """ cpid = int(cpid) to = int(to) - cp_track = frontend.backend.current_playlist.get(cpid=cpid) - position = frontend.backend.current_playlist.cp_tracks.index(cp_track) + cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() + position = frontend.backend.current_playlist.cp_tracks.get().index( + cp_track) frontend.backend.current_playlist.move(position, position + 1, to) @handle_pattern(r'^playlist$') @@ -189,9 +192,9 @@ def playlistfind(frontend, tag, needle): """ if tag == 'filename': try: - cp_track = frontend.backend.current_playlist.get(uri=needle) + cp_track = frontend.backend.current_playlist.get(uri=needle).get() (cpid, track) = cp_track - position = frontend.backend.current_playlist.cp_tracks.index( + position = frontend.backend.current_playlist.cp_tracks.get().index( cp_track) return track.mpd_format(cpid=cpid, position=position) except LookupError: @@ -211,14 +214,17 @@ def playlistid(frontend, cpid=None): if cpid is not None: try: cpid = int(cpid) - cp_track = frontend.backend.current_playlist.get(cpid=cpid) - position = frontend.backend.current_playlist.cp_tracks.index( + cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() + position = frontend.backend.current_playlist.cp_tracks.get().index( cp_track) return cp_track[1].mpd_format(position=position, cpid=cpid) except LookupError: raise MpdNoExistError(u'No such song', command=u'playlistid') else: - return frontend.backend.current_playlist.mpd_format() + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), cpids=cpids) @handle_pattern(r'^playlistinfo$') @handle_pattern(r'^playlistinfo "(?P-?\d+)"$') @@ -248,18 +254,27 @@ def playlistinfo(frontend, songpos=None, end = songpos + 1 if start == -1: end = None - return frontend.backend.current_playlist.mpd_format(start, end) + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), + start, end, cpids=cpids) else: if start is None: start = 0 start = int(start) - if not (0 <= start <= len(frontend.backend.current_playlist.tracks)): + if not (0 <= start <= len( + frontend.backend.current_playlist.tracks.get())): raise MpdArgError(u'Bad song index', command=u'playlistinfo') if end is not None: end = int(end) - if end > len(frontend.backend.current_playlist.tracks): + if end > len(frontend.backend.current_playlist.tracks.get()): end = None - return frontend.backend.current_playlist.mpd_format(start, end) + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), + start, end, cpids=cpids) @handle_pattern(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') @handle_pattern(r'^playlistsearch (?P\S+) "(?P[^"]+)"$') @@ -298,7 +313,10 @@ def plchanges(frontend, version): """ # XXX Naive implementation that returns all tracks as changed if int(version) < frontend.backend.current_playlist.version: - return frontend.backend.current_playlist.mpd_format() + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), cpids=cpids) @handle_pattern(r'^plchangesposid "(?P\d+)"$') def plchangesposid(frontend, version): @@ -315,10 +333,10 @@ def plchangesposid(frontend, version): ``playlistlength`` returned by status command. """ # XXX Naive implementation that returns all tracks as changed - if int(version) != frontend.backend.current_playlist.version: + if int(version) != frontend.backend.current_playlist.version.get(): result = [] for (position, (cpid, _)) in enumerate( - frontend.backend.current_playlist.cp_tracks): + frontend.backend.current_playlist.cp_tracks.get()): result.append((u'cpos', position)) result.append((u'Id', cpid)) return result @@ -351,7 +369,7 @@ def swap(frontend, songpos1, songpos2): """ songpos1 = int(songpos1) songpos2 = int(songpos2) - tracks = frontend.backend.current_playlist.tracks + tracks = frontend.backend.current_playlist.tracks.get() song1 = tracks[songpos1] song2 = tracks[songpos2] del tracks[songpos1] @@ -372,8 +390,9 @@ def swapid(frontend, cpid1, cpid2): """ cpid1 = int(cpid1) cpid2 = int(cpid2) - cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1) - cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2) - position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1) - position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2) + cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get() + cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get() + cp_tracks = frontend.backend.current_playlist.cp_tracks.get() + position1 = cp_tracks.index(cp_track1) + position2 = cp_tracks.index(cp_track2) swap(frontend, position1, position2) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index d50388f5..a6836533 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -68,7 +68,7 @@ def find(frontend, mpd_query): - also uses the search type "date". """ query = _build_query(mpd_query) - return frontend.backend.library.find_exact(**query).mpd_format() + return frontend.backend.library.find_exact(**query).get().mpd_format() @handle_pattern(r'^findadd ' r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? ' @@ -215,7 +215,7 @@ def _list_build_query(field, mpd_query): def _list_artist(frontend, query): artists = set() - playlist = frontend.backend.library.find_exact(**query) + playlist = frontend.backend.library.find_exact(**query).get() for track in playlist.tracks: for artist in track.artists: artists.add((u'Artist', artist.name)) @@ -223,7 +223,7 @@ def _list_artist(frontend, query): def _list_album(frontend, query): albums = set() - playlist = frontend.backend.library.find_exact(**query) + playlist = frontend.backend.library.find_exact(**query).get() for track in playlist.tracks: if track.album is not None: albums.add((u'Album', track.album.name)) @@ -231,7 +231,7 @@ def _list_album(frontend, query): def _list_date(frontend, query): dates = set() - playlist = frontend.backend.library.find_exact(**query) + playlist = frontend.backend.library.find_exact(**query).get() for track in playlist.tracks: if track.date is not None: dates.add((u'Date', track.date.strftime('%Y-%m-%d'))) @@ -324,7 +324,7 @@ def search(frontend, mpd_query): - also uses the search type "date". """ query = _build_query(mpd_query) - return frontend.backend.library.search(**query).mpd_format() + return frontend.backend.library.search(**query).get().mpd_format() @handle_pattern(r'^update( "(?P[^"]+)")*$') def update(frontend, uri=None, rescan_unmodified_files=False): diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index d009969d..65282f42 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,3 +1,4 @@ +from mopidy.backends.base import PlaybackController from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @@ -86,7 +87,7 @@ def next_(frontend): order as the first time. """ - return frontend.backend.playback.next() + return frontend.backend.playback.next().get() @handle_pattern(r'^pause$') @handle_pattern(r'^pause "(?P[01])"$') @@ -103,11 +104,11 @@ def pause(frontend, state=None): - Calls ``pause`` without any arguments to toogle pause. """ if state is None: - if (frontend.backend.playback.state == - frontend.backend.playback.PLAYING): + if (frontend.backend.playback.state.get() == + PlaybackController.PLAYING): frontend.backend.playback.pause() - elif (frontend.backend.playback.state == - frontend.backend.playback.PAUSED): + elif (frontend.backend.playback.state.get() == + PlaybackController.PAUSED): frontend.backend.playback.resume() elif int(state): frontend.backend.playback.pause() @@ -120,7 +121,7 @@ def play(frontend): The original MPD server resumes from the paused state on ``play`` without arguments. """ - return frontend.backend.playback.play() + return frontend.backend.playback.play().get() @handle_pattern(r'^playid "(?P\d+)"$') @handle_pattern(r'^playid "(?P-1)"$') @@ -145,8 +146,8 @@ def playid(frontend, cpid): if cpid == -1: return _play_minus_one(frontend) try: - cp_track = frontend.backend.current_playlist.get(cpid=cpid) - return frontend.backend.playback.play(cp_track) + cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() + return frontend.backend.playback.play(cp_track).get() except LookupError: raise MpdNoExistError(u'No such song', command=u'playid') @@ -177,22 +178,22 @@ def playpos(frontend, songpos): if songpos == -1: return _play_minus_one(frontend) try: - cp_track = frontend.backend.current_playlist.cp_tracks[songpos] - return frontend.backend.playback.play(cp_track) + cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos] + return frontend.backend.playback.play(cp_track).get() except IndexError: raise MpdArgError(u'Bad song index', command=u'play') def _play_minus_one(frontend): - if (frontend.backend.playback.state == frontend.backend.playback.PLAYING): + if (frontend.backend.playback.state.get() == PlaybackController.PLAYING): return # Nothing to do - elif (frontend.backend.playback.state == frontend.backend.playback.PAUSED): - return frontend.backend.playback.resume() - elif frontend.backend.playback.current_cp_track is not None: - cp_track = frontend.backend.playback.current_cp_track - return frontend.backend.playback.play(cp_track) - elif frontend.backend.current_playlist.cp_tracks: - cp_track = frontend.backend.current_playlist.cp_tracks[0] - return frontend.backend.playback.play(cp_track) + elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED): + return frontend.backend.playback.resume().get() + elif frontend.backend.playback.current_cp_track.get() is not None: + cp_track = frontend.backend.playback.current_cp_track.get() + return frontend.backend.playback.play(cp_track).get() + elif frontend.backend.current_playlist.cp_tracks.get(): + cp_track = frontend.backend.current_playlist.cp_tracks.get()[0] + return frontend.backend.playback.play(cp_track).get() else: return # Fail silently @@ -240,7 +241,7 @@ def previous(frontend): ``previous`` should do a seek to time position 0. """ - return frontend.backend.playback.previous() + return frontend.backend.playback.previous().get() @handle_pattern(r'^random (?P[01])$') @handle_pattern(r'^random "(?P[01])"$') @@ -351,7 +352,7 @@ def setvol(frontend, volume): volume = 0 if volume > 100: volume = 100 - frontend.backend.mixer.volume = volume + frontend.mixer.volume = volume @handle_pattern(r'^single (?P[01])$') @handle_pattern(r'^single "(?P[01])"$') diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 83efdd94..ab782440 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -81,4 +81,4 @@ def urlhandlers(frontend): Gets a list of available URL handlers. """ - return [(u'handler', uri) for uri in frontend.backend.uri_handlers] + return [(u'handler', uri) for uri in frontend.backend.uri_handlers.get()] diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index e18f1ea4..a78efc0a 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,3 +1,4 @@ +from mopidy.backends.base import PlaybackController from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import MpdNotImplemented @@ -23,10 +24,11 @@ def currentsong(frontend): Displays the song info of the current song (same song that is identified in status). """ - if frontend.backend.playback.current_track is not None: - return frontend.backend.playback.current_track.mpd_format( - position=frontend.backend.playback.current_playlist_position, - cpid=frontend.backend.playback.current_cpid) + current_cp_track = frontend.backend.playback.current_cp_track.get() + if current_cp_track is not None: + return current_cp_track[1].mpd_format( + position=frontend.backend.playback.current_playlist_position.get(), + cpid=current_cp_track[0]) @handle_pattern(r'^idle$') @handle_pattern(r'^idle (?P.+)$') @@ -90,8 +92,7 @@ def stats(frontend): 'artists': 0, # TODO 'albums': 0, # TODO 'songs': 0, # TODO - # TODO Does not work after multiprocessing branch merge - 'uptime': 0, # frontend.session.stats_uptime(), + 'uptime': 0, # TODO 'db_playtime': 0, # TODO 'db_update': 0, # TODO 'playtime': 0, # TODO @@ -140,56 +141,59 @@ def status(frontend): ('xfade', _status_xfade(frontend)), ('state', _status_state(frontend)), ] - if frontend.backend.playback.current_track is not None: + if frontend.backend.playback.current_track.get() is not None: result.append(('song', _status_songpos(frontend))) result.append(('songid', _status_songid(frontend))) - if frontend.backend.playback.state in (frontend.backend.playback.PLAYING, - frontend.backend.playback.PAUSED): + if frontend.backend.playback.state.get() in (PlaybackController.PLAYING, + PlaybackController.PAUSED): result.append(('time', _status_time(frontend))) result.append(('elapsed', _status_time_elapsed(frontend))) result.append(('bitrate', _status_bitrate(frontend))) return result def _status_bitrate(frontend): - if frontend.backend.playback.current_track is not None: - return frontend.backend.playback.current_track.bitrate + current_track = frontend.backend.playback.current_track.get() + if current_track is not None: + return current_track.bitrate def _status_consume(frontend): - if frontend.backend.playback.consume: + if frontend.backend.playback.consume.get(): return 1 else: return 0 def _status_playlist_length(frontend): - return len(frontend.backend.current_playlist.tracks) + return len(frontend.backend.current_playlist.tracks.get()) def _status_playlist_version(frontend): - return frontend.backend.current_playlist.version + return frontend.backend.current_playlist.version.get() def _status_random(frontend): - return int(frontend.backend.playback.random) + return int(frontend.backend.playback.random.get()) def _status_repeat(frontend): - return int(frontend.backend.playback.repeat) + return int(frontend.backend.playback.repeat.get()) def _status_single(frontend): - return int(frontend.backend.playback.single) + return int(frontend.backend.playback.single.get()) def _status_songid(frontend): - if frontend.backend.playback.current_cpid is not None: - return frontend.backend.playback.current_cpid + current_cpid = frontend.backend.playback.current_cpid.get() + if current_cpid is not None: + return current_cpid else: return _status_songpos(frontend) def _status_songpos(frontend): - return frontend.backend.playback.current_playlist_position + return frontend.backend.playback.current_playlist_position.get() def _status_state(frontend): - if frontend.backend.playback.state == frontend.backend.playback.PLAYING: + state = frontend.backend.playback.state.get() + if state == PlaybackController.PLAYING: return u'play' - elif frontend.backend.playback.state == frontend.backend.playback.STOPPED: + elif state == PlaybackController.STOPPED: return u'stop' - elif frontend.backend.playback.state == frontend.backend.playback.PAUSED: + elif state == PlaybackController.PAUSED: return u'pause' def _status_time(frontend): @@ -197,19 +201,21 @@ def _status_time(frontend): _status_time_total(frontend) // 1000) def _status_time_elapsed(frontend): - return frontend.backend.playback.time_position + return frontend.backend.playback.time_position.get() def _status_time_total(frontend): - if frontend.backend.playback.current_track is None: + current_track = frontend.backend.playback.current_track.get() + if current_track is None: return 0 - elif frontend.backend.playback.current_track.length is None: + elif current_track.length is None: return 0 else: - return frontend.backend.playback.current_track.length + return current_track.length def _status_volume(frontend): - if frontend.backend.mixer.volume is not None: - return frontend.backend.mixer.volume + volume = frontend.mixer.volume.get() + if volume is not None: + return volume else: return 0 diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index c34b1676..6eccffac 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -19,8 +19,8 @@ def listplaylist(frontend, name): file: relative/path/to/file3.mp3 """ try: - return ['file: %s' % t.uri - for t in frontend.backend.stored_playlists.get(name=name).tracks] + playlist = frontend.backend.stored_playlists.get(name=name).get() + return ['file: %s' % t.uri for t in playlist.tracks] except LookupError: raise MpdNoExistError(u'No such playlist', command=u'listplaylist') @@ -39,7 +39,8 @@ def listplaylistinfo(frontend, name): Album, Artist, Track """ try: - return frontend.backend.stored_playlists.get(name=name).mpd_format() + playlist = frontend.backend.stored_playlists.get(name=name).get() + return playlist.mpd_format() except LookupError: raise MpdNoExistError( u'No such playlist', command=u'listplaylistinfo') @@ -66,7 +67,7 @@ def listplaylists(frontend): Last-Modified: 2010-02-06T02:11:08Z """ result = [] - for playlist in frontend.backend.stored_playlists.playlists: + for playlist in frontend.backend.stored_playlists.playlists.get(): result.append((u'playlist', playlist.name)) last_modified = (playlist.last_modified or dt.datetime.now()).isoformat() @@ -92,7 +93,7 @@ def load(frontend, name): - ``load`` appends the given playlist to the current playlist. """ try: - playlist = frontend.backend.stored_playlists.get(name=name) + playlist = frontend.backend.stored_playlists.get(name=name).get() frontend.backend.current_playlist.append(playlist.tracks) except LookupError: raise MpdNoExistError(u'No such playlist', command=u'load') diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 7caf21f9..231bdf40 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -15,9 +15,8 @@ class MpdServer(asyncore.dispatcher): for each client connection. """ - def __init__(self, core_queue): + def __init__(self): asyncore.dispatcher.__init__(self) - self.core_queue = core_queue def start(self): """Start MPD server.""" @@ -47,8 +46,7 @@ class MpdServer(asyncore.dispatcher): (client_socket, client_socket_address) = self.accept() logger.info(u'MPD client connection from [%s]:%s', client_socket_address[0], client_socket_address[1]) - MpdSession(self, client_socket, client_socket_address, - self.core_queue).start() + MpdSession(self, client_socket, client_socket_address).start() def handle_close(self): """Handle end of client connection.""" diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index e8e3291d..5a473eca 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -1,30 +1,28 @@ import asynchat import logging -import multiprocessing from mopidy import settings +from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION from mopidy.utils.log import indent -from mopidy.utils.process import pickle_connection logger = logging.getLogger('mopidy.frontends.mpd.session') class MpdSession(asynchat.async_chat): """ - The MPD client session. Keeps track of a single client and passes its - MPD requests to the dispatcher. + The MPD client session. Keeps track of a single client session. Any + requests from the client is passed on to the MPD request dispatcher. """ - def __init__(self, server, client_socket, client_socket_address, - core_queue): + def __init__(self, server, client_socket, client_socket_address): asynchat.async_chat.__init__(self, sock=client_socket) self.server = server self.client_address = client_socket_address[0] self.client_port = client_socket_address[1] - self.core_queue = core_queue self.input_buffer = [] self.authenticated = False self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) + self.dispatcher = MpdDispatcher() def start(self): """Start a new client session.""" @@ -53,15 +51,7 @@ class MpdSession(asynchat.async_chat): if response is not None: self.send_response(response) return - my_end, other_end = multiprocessing.Pipe() - self.core_queue.put({ - 'to': 'frontend', - 'command': 'mpd_request', - 'request': request, - 'reply_to': pickle_connection(other_end), - }) - my_end.poll(None) - response = my_end.recv() + response = self.dispatcher.handle_request(request) if response is not None: self.handle_response(response) diff --git a/mopidy/frontends/mpd/thread.py b/mopidy/frontends/mpd/thread.py deleted file mode 100644 index 0ad5ee68..00000000 --- a/mopidy/frontends/mpd/thread.py +++ /dev/null @@ -1,18 +0,0 @@ -import asyncore -import logging - -from mopidy.frontends.mpd.server import MpdServer -from mopidy.utils.process import BaseThread - -logger = logging.getLogger('mopidy.frontends.mpd.thread') - -class MpdThread(BaseThread): - def __init__(self, core_queue): - super(MpdThread, self).__init__(core_queue) - self.name = u'MpdThread' - - def run_inside_try(self): - logger.debug(u'Starting MPD server thread') - server = MpdServer(self.core_queue) - server.start() - asyncore.loop() diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index 4aa5952f..6329bbbb 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -1,12 +1,14 @@ import alsaaudio import logging +from pykka.actor import ThreadingActor + from mopidy import settings from mopidy.mixers.base import BaseMixer logger = logging.getLogger('mopidy.mixers.alsa') -class AlsaMixer(BaseMixer): +class AlsaMixer(ThreadingActor, BaseMixer): """ Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control volume. @@ -20,8 +22,10 @@ class AlsaMixer(BaseMixer): - :attr:`mopidy.settings.MIXER_ALSA_CONTROL` """ - def __init__(self, *args, **kwargs): - super(AlsaMixer, self).__init__(*args, **kwargs) + def __init__(self): + self._mixer = None + + def on_start(self): self._mixer = alsaaudio.Mixer(self._get_mixer_control()) assert self._mixer is not None diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index f7f9525c..74996cb6 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -2,17 +2,12 @@ from mopidy import settings class BaseMixer(object): """ - :param backend: a backend instance - :type backend: :class:`mopidy.backends.base.Backend` - **Settings:** - :attr:`mopidy.settings.MIXER_MAX_VOLUME` """ - def __init__(self, backend, *args, **kwargs): - self.backend = backend - self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 + amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 @property def volume(self): @@ -35,9 +30,6 @@ class BaseMixer(object): volume = 100 self._set_volume(volume) - def destroy(self): - pass - def _get_volume(self): """ Return volume as integer in range [0, 100]. :class:`None` if unknown. diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index f0712f95..3922d20a 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -1,12 +1,13 @@ import logging -from threading import Lock + +from pykka.actor import ThreadingActor from mopidy import settings from mopidy.mixers.base import BaseMixer logger = logging.getLogger(u'mopidy.mixers.denon') -class DenonMixer(BaseMixer): +class DenonMixer(ThreadingActor, BaseMixer): """ Mixer for controlling Denon amplifiers and receivers using the RS-232 protocol. @@ -25,27 +26,19 @@ class DenonMixer(BaseMixer): """ def __init__(self, *args, **kwargs): - """ - Connects using the serial specifications from Denon's RS-232 Protocol - specification: 9600bps 8N1. - """ - super(DenonMixer, self).__init__(*args, **kwargs) - device = kwargs.get('device', None) - if device: - self._device = device - else: - from serial import Serial - self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) + self._device = kwargs.get('device', None) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 - self._lock = Lock() + + def on_start(self): + if self._device is None: + from serial import Serial + self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) def _get_volume(self): - self._lock.acquire() - self.ensure_open_device() + self._ensure_open_device() self._device.write('MV?\r') vol = str(self._device.readline()[2:4]) - self._lock.release() logger.debug(u'_get_volume() = %s' % vol) return self._levels.index(vol) @@ -53,14 +46,12 @@ class DenonMixer(BaseMixer): # Clamp according to Denon-spec if volume > 99: volume = 99 - self._lock.acquire() - self.ensure_open_device() + self._ensure_open_device() self._device.write('MV%s\r'% self._levels[volume]) vol = self._device.readline()[2:4] - self._lock.release() self._volume = self._levels.index(vol) - def ensure_open_device(self): + def _ensure_open_device(self): if not self._device.isOpen(): logger.debug(u'(re)connecting to Denon device') self._device.open() diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py index 12a8137e..186bc7aa 100644 --- a/mopidy/mixers/dummy.py +++ b/mopidy/mixers/dummy.py @@ -1,10 +1,11 @@ +from pykka.actor import ThreadingActor + from mopidy.mixers.base import BaseMixer -class DummyMixer(BaseMixer): +class DummyMixer(ThreadingActor, BaseMixer): """Mixer which just stores and reports the chosen volume.""" - def __init__(self, *args, **kwargs): - super(DummyMixer, self).__init__(*args, **kwargs) + def __init__(self): self._volume = None def _get_volume(self): diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 9dca3690..d6365b4b 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -1,13 +1,22 @@ -from mopidy.mixers.base import BaseMixer +from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry -class GStreamerSoftwareMixer(BaseMixer): +from mopidy.mixers.base import BaseMixer +from mopidy.outputs.base import BaseOutput + +class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): """Mixer which uses GStreamer to control volume in software.""" - def __init__(self, *args, **kwargs): - super(GStreamerSoftwareMixer, self).__init__(*args, **kwargs) + def __init__(self): + self.output = None + + def on_start(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() def _get_volume(self): - return self.backend.output.get_volume() + return self.output.get_volume().get() def _set_volume(self, volume): - self.backend.output.set_volume(volume) + self.output.set_volume(volume).get() diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 5cf92826..bd53376e 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -1,14 +1,14 @@ import logging -from serial import Serial -from multiprocessing import Pipe +import serial + +from pykka.actor import ThreadingActor from mopidy import settings from mopidy.mixers.base import BaseMixer -from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.mixers.nad') -class NadMixer(BaseMixer): +class NadMixer(ThreadingActor, BaseMixer): """ Mixer for controlling NAD amplifiers and receivers using the NAD RS-232 protocol. @@ -36,21 +36,19 @@ class NadMixer(BaseMixer): """ - def __init__(self, *args, **kwargs): - super(NadMixer, self).__init__(*args, **kwargs) - self._volume = None - self._pipe, other_end = Pipe() - NadTalker(self.backend.core_queue, pipe=other_end).start() + def __init__(self): + self._volume_cache = None + self._nad_talker = NadTalker.start().proxy() def _get_volume(self): - return self._volume + return self._volume_cache def _set_volume(self, volume): - self._volume = volume - self._pipe.send({'command': 'set_volume', 'volume': volume}) + self._volume_cache = volume + self._nad_talker.set_volume(volume) -class NadTalker(BaseThread): +class NadTalker(ThreadingActor): """ Independent process which does the communication with the NAD device. @@ -72,29 +70,20 @@ class NadTalker(BaseThread): # Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration. _nad_volume = None - def __init__(self, core_queue, pipe=None): - super(NadTalker, self).__init__(core_queue) - self.name = u'NadTalker' - self.pipe = pipe + def __init__(self): self._device = None - def run_inside_try(self): + def on_start(self): self._open_connection() self._set_device_to_known_state() - while self.pipe.poll(None): - message = self.pipe.recv() - if message['command'] == 'set_volume': - self._set_volume(message['volume']) - elif message['command'] == 'reset_device': - self._set_device_to_known_state() def _open_connection(self): # Opens serial connection to the device. # Communication settings: 115200 bps 8N1 logger.info(u'Connecting to serial device "%s"', settings.MIXER_EXT_PORT) - self._device = Serial(port=settings.MIXER_EXT_PORT, baudrate=115200, - timeout=self.TIMEOUT) + self._device = serial.Serial(port=settings.MIXER_EXT_PORT, + baudrate=115200, timeout=self.TIMEOUT) self._get_device_model() def _set_device_to_known_state(self): @@ -164,7 +153,7 @@ class NadTalker(BaseThread): self._nad_volume = 0 logger.info(u'Done calibrating NAD amplifier') - def _set_volume(self, volume): + def set_volume(self, volume): # Increase or decrease the amplifier volume until it matches the given # target volume. logger.debug(u'Setting volume to %d' % volume) diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py index 2ea04cf2..53983095 100644 --- a/mopidy/mixers/osa.py +++ b/mopidy/mixers/osa.py @@ -1,9 +1,11 @@ from subprocess import Popen, PIPE import time +from pykka.actor import ThreadingActor + from mopidy.mixers.base import BaseMixer -class OsaMixer(BaseMixer): +class OsaMixer(ThreadingActor, BaseMixer): """ Mixer which uses ``osascript`` on OS X to control volume. @@ -14,7 +16,6 @@ class OsaMixer(BaseMixer): **Settings:** - None - """ CACHE_TTL = 30 diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py index 372d7d70..fbc86688 100644 --- a/mopidy/outputs/base.py +++ b/mopidy/outputs/base.py @@ -3,33 +3,6 @@ class BaseOutput(object): Base class for audio outputs. """ - def __init__(self, core_queue): - self.core_queue = core_queue - - def start(self): - """ - Start the output. - - *MAY be implemented by subclasses.* - """ - pass - - def destroy(self): - """ - Destroy the output. - - *MAY be implemented by subclasses.* - """ - pass - - def process_message(self, message): - """ - Process messages with the output as destination. - - *MUST be implemented by subclass.* - """ - raise NotImplementedError - def play_uri(self, uri): """ Play URI. diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py index 060ee02f..f09965f7 100644 --- a/mopidy/outputs/dummy.py +++ b/mopidy/outputs/dummy.py @@ -1,6 +1,8 @@ +from pykka.actor import ThreadingActor + from mopidy.outputs.base import BaseOutput -class DummyOutput(BaseOutput): +class DummyOutput(ThreadingActor, BaseOutput): """ Audio output used for testing. """ @@ -8,15 +10,6 @@ class DummyOutput(BaseOutput): # pylint: disable = R0902 # Too many instance attributes (9/7) - #: For testing. :class:`True` if :meth:`start` has been called. - start_called = False - - #: For testing. :class:`True` if :meth:`destroy` has been called. - destroy_called = False - - #: For testing. Contains all messages :meth:`process_message` has received. - messages = [] - #: For testing. Contains the last URI passed to :meth:`play_uri`. uri = None @@ -40,15 +33,6 @@ class DummyOutput(BaseOutput): #: For testing. Contains the current volume. volume = 100 - def start(self): - self.start_called = True - - def destroy(self): - self.destroy_called = True - - def process_message(self, message): - self.messages.append(message) - def play_uri(self, uri): self.uri = uri return True diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 3b037f62..0596addb 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -3,113 +3,39 @@ pygst.require('0.10') import gst import logging -import multiprocessing + +from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry from mopidy import settings +from mopidy.backends.base import Backend from mopidy.outputs.base import BaseOutput -from mopidy.utils.process import (BaseThread, pickle_connection, - unpickle_connection) logger = logging.getLogger('mopidy.outputs.gstreamer') -class GStreamerOutput(BaseOutput): +class GStreamerOutput(ThreadingActor, BaseOutput): """ - Audio output through GStreamer. - - Starts :class:`GStreamerMessagesThread` and :class:`GStreamerPlayerThread`. + Audio output through `GStreamer `_. **Settings:** - :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK` + """ - def __init__(self, *args, **kwargs): - super(GStreamerOutput, self).__init__(*args, **kwargs) - self.output_queue = multiprocessing.Queue() - self.player_thread = GStreamerPlayerThread(self.core_queue, - self.output_queue) - - def start(self): - self.player_thread.start() - - def destroy(self): - self.player_thread.destroy() - - def process_message(self, message): - assert message['to'] == 'output', \ - u'Message recipient must be "output".' - self.output_queue.put(message) - - def _send_recv(self, message): - (my_end, other_end) = multiprocessing.Pipe() - message['to'] = 'output' - message['reply_to'] = pickle_connection(other_end) - self.process_message(message) - my_end.poll(None) - return my_end.recv() - - def _send(self, message): - message['to'] = 'output' - self.process_message(message) - - def play_uri(self, uri): - return self._send_recv({'command': 'play_uri', 'uri': uri}) - - def deliver_data(self, capabilities, data): - return self._send({ - 'command': 'deliver_data', - 'caps': capabilities, - 'data': data, - }) - - def end_of_data_stream(self): - return self._send({'command': 'end_of_data_stream'}) - - def get_position(self): - return self._send_recv({'command': 'get_position'}) - - def set_position(self, position): - return self._send_recv({'command': 'set_position', - 'position': position}) - - def set_state(self, state): - return self._send_recv({'command': 'set_state', 'state': state}) - - def get_volume(self): - return self._send_recv({'command': 'get_volume'}) - - def set_volume(self, volume): - return self._send_recv({'command': 'set_volume', 'volume': volume}) - - -class GStreamerPlayerThread(BaseThread): - """ - A process for all work related to GStreamer. - - The main loop processes events from both Mopidy and GStreamer. - - This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be - running too. This is not enforced in any way by the code. - - Make sure this subprocess is started by the MainThread in the top-most - parent process, and not some other thread. If not, we can get into the - problems described at - http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html. - """ - - def __init__(self, core_queue, output_queue): - super(GStreamerPlayerThread, self).__init__(core_queue) - self.name = u'GStreamerPlayerThread' - self.output_queue = output_queue + def __init__(self): self.gst_pipeline = None - def run_inside_try(self): - self.setup() - while True: - message = self.output_queue.get() - self.process_mopidy_message(message) + def on_start(self): + self._setup_gstreamer() + + def _setup_gstreamer(self): + """ + **Warning:** :class:`GStreamerOutput` requires + :class:`mopidy.utils.process.GObjectEventThread` to be running. This is + not enforced by :class:`GStreamerOutput` itself. + """ - def setup(self): logger.debug(u'Setting up GStreamer pipeline') self.gst_pipeline = gst.parse_launch(' ! '.join([ @@ -122,7 +48,7 @@ class GStreamerPlayerThread(BaseThread): if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend': uri_bin = gst.element_factory_make('uridecodebin', 'uri') - uri_bin.connect('pad-added', self.process_new_pad, pad) + uri_bin.connect('pad-added', self._process_new_pad, pad) self.gst_pipeline.add(uri_bin) else: app_src = gst.element_factory_make('appsrc', 'appsrc') @@ -141,57 +67,29 @@ class GStreamerPlayerThread(BaseThread): # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() gst_bus.add_signal_watch() - gst_bus.connect('message', self.process_gst_message) + gst_bus.connect('message', self._process_gstreamer_message) - def process_new_pad(self, source, pad, target_pad): + def _process_new_pad(self, source, pad, target_pad): pad.link(target_pad) - def process_mopidy_message(self, message): - """Process messages from the rest of Mopidy.""" - if message['command'] == 'play_uri': - response = self.play_uri(message['uri']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'deliver_data': - self.deliver_data(message['caps'], message['data']) - elif message['command'] == 'end_of_data_stream': - self.end_of_data_stream() - elif message['command'] == 'set_state': - response = self.set_state(message['state']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'get_volume': - volume = self.get_volume() - connection = unpickle_connection(message['reply_to']) - connection.send(volume) - elif message['command'] == 'set_volume': - response = self.set_volume(message['volume']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'set_position': - response = self.set_position(message['position']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'get_position': - response = self.get_position() - connection = unpickle_connection(message['reply_to']) - connection.send(response) - else: - logger.warning(u'Cannot handle message: %s', message) - - def process_gst_message(self, bus, message): + def _process_gstreamer_message(self, bus, message): """Process messages from GStreamer.""" if message.type == gst.MESSAGE_EOS: logger.debug(u'GStreamer signalled end-of-stream. ' - 'Sending end_of_track to core_queue ...') - self.core_queue.put({'command': 'end_of_track'}) + 'Telling backend ...') + self._get_backend().playback.on_end_of_track() elif message.type == gst.MESSAGE_ERROR: self.set_state('NULL') error, debug = message.parse_error() logger.error(u'%s %s', error, debug) - # FIXME Should we send 'stop_playback' to core here? Can we + # FIXME Should we send 'stop_playback' to the backend here? Can we # differentiate on how serious the error is? + def _get_backend(self): + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + return backend_refs[0].proxy() + def play_uri(self, uri): """Play audio at URI""" self.set_state('READY') @@ -216,6 +114,21 @@ class GStreamerPlayerThread(BaseThread): """ self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream') + def get_position(self): + try: + position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] + return position // gst.MSECOND + except gst.QueryError, e: + logger.error('time_position failed: %s', e) + return 0 + + def set_position(self, position): + self.gst_pipeline.get_state() # block until state changes are done + handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME), + gst.SEEK_FLAG_FLUSH, position * gst.MSECOND) + self.gst_pipeline.get_state() # block until seek is done + return handeled + def set_state(self, state_name): """ Set the GStreamer state. Returns :class:`True` if successful. @@ -252,18 +165,3 @@ class GStreamerPlayerThread(BaseThread): gst_volume = self.gst_pipeline.get_by_name('volume') gst_volume.set_property('volume', volume / 100.0) return True - - def set_position(self, position): - self.gst_pipeline.get_state() # block until state changes are done - handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME), - gst.SEEK_FLAG_FLUSH, position * gst.MSECOND) - self.gst_pipeline.get_state() # block until seek is done - return handeled - - def get_position(self): - try: - position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] - return position // gst.MSECOND - except gst.QueryError, e: - logger.error('time_position failed: %s', e) - return 0 diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index cc1c19c1..c74ff5ea 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -1,13 +1,15 @@ import logging import logging.handlers -from mopidy import settings +from mopidy import get_version, settings def setup_logging(verbosity_level, save_debug_log): setup_root_logger() setup_console_logging(verbosity_level) if save_debug_log: setup_debug_logging_to_file() + logger = logging.getLogger('mopidy.utils.log') + logger.info(u'-- Starting Mopidy %s --', get_version()) def setup_root_logger(): root = logging.getLogger('') diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 11dafa8a..dbc6cada 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -1,8 +1,5 @@ import logging -import multiprocessing -import multiprocessing.dummy -from multiprocessing.reduction import reduce_connection -import pickle +import threading import gobject gobject.threads_init() @@ -11,52 +8,10 @@ from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') -def pickle_connection(connection): - return pickle.dumps(reduce_connection(connection)) -def unpickle_connection(pickled_connection): - # From http://stackoverflow.com/questions/1446004 - (func, args) = pickle.loads(pickled_connection) - return func(*args) - -class BaseProcess(multiprocessing.Process): - def __init__(self, core_queue): - super(BaseProcess, self).__init__() - self.core_queue = core_queue - - def run(self): - logger.debug(u'%s: Starting process', self.name) - try: - self.run_inside_try() - except KeyboardInterrupt: - logger.info(u'Interrupted by user') - self.exit(0, u'Interrupted by user') - except SettingsError as e: - logger.error(e.message) - self.exit(1, u'Settings error') - except ImportError as e: - logger.error(e) - self.exit(2, u'Import error') - except Exception as e: - logger.exception(e) - self.exit(3, u'Unknown error') - - def run_inside_try(self): - raise NotImplementedError - - def destroy(self): - self.terminate() - - def exit(self, status=0, reason=None): - self.core_queue.put({'to': 'core', 'command': 'exit', - 'status': status, 'reason': reason}) - self.destroy() - - -class BaseThread(multiprocessing.dummy.Process): - def __init__(self, core_queue): +class BaseThread(threading.Thread): + def __init__(self): super(BaseThread, self).__init__() - self.core_queue = core_queue # No thread should block process from exiting self.daemon = True @@ -84,8 +39,6 @@ class BaseThread(multiprocessing.dummy.Process): pass def exit(self, status=0, reason=None): - self.core_queue.put({'to': 'core', 'command': 'exit', - 'status': status, 'reason': reason}) self.destroy() @@ -98,8 +51,8 @@ class GObjectEventThread(BaseThread): :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc. """ - def __init__(self, core_queue): - super(GObjectEventThread, self).__init__(core_queue) + def __init__(self): + super(GObjectEventThread, self).__init__() self.name = u'GObjectEventThread' self.loop = None diff --git a/requirements/core.txt b/requirements/core.txt new file mode 100644 index 00000000..aaae84f8 --- /dev/null +++ b/requirements/core.txt @@ -0,0 +1 @@ +Pykka >= 0.12 diff --git a/requirements/tests.txt b/requirements/tests.txt index 71dab096..f8cf2eb3 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,3 +1,4 @@ coverage +mock nose tox diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index c2a9df6f..ee5e1111 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -1,9 +1,9 @@ +import mock import multiprocessing import random -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track -from mopidy.outputs.dummy import DummyOutput +from mopidy.outputs.base import BaseOutput from tests.backends.base import populate_playlist @@ -11,19 +11,13 @@ class CurrentPlaylistControllerTest(object): tracks = [] def setUp(self): - self.core_queue = multiprocessing.Queue() - self.output = DummyOutput(self.core_queue) - self.backend = self.backend_class( - self.core_queue, self.output, DummyMixer) + self.backend = self.backend_class() + self.backend.output = mock.Mock(spec=BaseOutput) self.controller = self.backend.current_playlist self.playback = self.backend.playback assert len(self.tracks) == 3, 'Need three tracks to run tests.' - def tearDown(self): - self.backend.destroy() - self.output.destroy() - def test_add(self): for track in self.tracks: cp_track = self.controller.add(track) diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index 71f62147..bff26c4c 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -1,4 +1,3 @@ -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track, Album, Artist from tests import SkipTest, data_folder @@ -15,12 +14,9 @@ class LibraryControllerTest(object): Track()] def setUp(self): - self.backend = self.backend_class(mixer_class=DummyMixer) + self.backend = self.backend_class() self.library = self.backend.library - def tearDown(self): - self.backend.destroy() - def test_refresh(self): self.library.refresh() diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 77eb012d..8ea48a3a 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -1,10 +1,10 @@ +import mock import multiprocessing import random import time -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track -from mopidy.outputs.dummy import DummyOutput +from mopidy.outputs.base import BaseOutput from tests import SkipTest from tests.backends.base import populate_playlist @@ -15,10 +15,8 @@ class PlaybackControllerTest(object): tracks = [] def setUp(self): - self.core_queue = multiprocessing.Queue() - self.output = DummyOutput(self.core_queue) - self.backend = self.backend_class( - self.core_queue, self.output, DummyMixer) + self.backend = self.backend_class() + self.backend.output = mock.Mock(spec=BaseOutput) self.playback = self.backend.playback self.current_playlist = self.backend.current_playlist @@ -27,10 +25,6 @@ class PlaybackControllerTest(object): assert self.tracks[0].length >= 2000, \ 'First song needs to be at least 2000 miliseconds' - def tearDown(self): - self.backend.destroy() - self.output.destroy() - def test_initial_state_is_stopped(self): self.assertEqual(self.playback.state, self.playback.STOPPED) @@ -733,10 +727,18 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.stop(), None) def test_time_position_when_stopped(self): + future = mock.Mock() + future.get = mock.Mock(return_value=0) + self.backend.output.get_position = mock.Mock(return_value=future) + self.assertEqual(self.playback.time_position, 0) @populate_playlist def test_time_position_when_stopped_with_playlist(self): + future = mock.Mock() + future.get = mock.Mock(return_value=0) + self.backend.output.get_position = mock.Mock(return_value=future) + self.assertEqual(self.playback.time_position, 0) @SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index 0ac0b167..12e48256 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -3,7 +3,6 @@ import shutil import tempfile from mopidy import settings -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist from tests import SkipTest, data_folder @@ -14,12 +13,10 @@ class StoredPlaylistsControllerTest(object): settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') settings.LOCAL_MUSIC_PATH = data_folder('') - self.backend = self.backend_class(mixer_class=DummyMixer) + self.backend = self.backend_class() self.stored = self.backend.stored_playlists def tearDown(self): - self.backend.destroy() - if os.path.exists(settings.LOCAL_PLAYLIST_PATH): shutil.rmtree(settings.LOCAL_PLAYLIST_PATH) diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index a7d9043f..5bc16d1c 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -70,8 +70,7 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest, self.stored.save(playlist) - self.backend.destroy() - self.backend = self.backend_class(mixer_class=DummyMixer) + self.backend = self.backend_class() self.stored = self.backend.stored_playlists self.assert_(self.stored.playlists) diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py index b81e727e..afa99d26 100644 --- a/tests/frontends/mpd/audio_output_test.py +++ b/tests/frontends/mpd/audio_output_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_enableoutput(self): result = self.h.handle_request(u'enableoutput "0"') @@ -20,6 +25,6 @@ class AudioOutputHandlerTest(unittest.TestCase): def test_outputs(self): result = self.h.handle_request(u'outputs') self.assert_(u'outputid: 0' in result) - self.assert_(u'outputname: DummyBackend' in result) + self.assert_(u'outputname: None' in result) self.assert_(u'outputenabled: 1' in result) self.assert_(u'OK' in result) diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py index 6c801c3f..7ff96bac 100644 --- a/tests/frontends/mpd/command_list_test.py +++ b/tests/frontends/mpd/command_list_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class CommandListsTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_command_list_begin(self): result = self.h.handle_request(u'command_list_begin') diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 44ce78ca..cf161a5a 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -7,10 +7,13 @@ from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() settings.runtime.clear() def test_close(self): diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index a4179637..eb113ed7 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -7,26 +7,26 @@ from mopidy.models import Track class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_add(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'add "dummy://foo"') - self.assertEqual(len(self.b.current_playlist.tracks), 6) - self.assertEqual(self.b.current_playlist.tracks[5], needle) self.assertEqual(len(result), 1) - self.assert_(u'OK' in result) - - def test_add_with_uri_not_found_in_library_should_not_call_lookup(self): - self.b.library.lookup = lambda uri: self.fail("Shouldn't run") - result = self.h.handle_request(u'add "foo"') - self.assertEqual(result[0], - u'ACK [50@0] {add} directory or file not found') + self.assertEqual(result[0], u'OK') + self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) + self.assertEqual(self.b.current_playlist.tracks.get()[5], needle) def test_add_with_uri_not_found_in_library_should_ack(self): result = self.h.handle_request(u'add "dummy://foo"') @@ -40,41 +40,43 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_without_songpos(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'addid "dummy://foo"') - self.assertEqual(len(self.b.current_playlist.tracks), 6) - self.assertEqual(self.b.current_playlist.tracks[5], needle) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[5][0] + self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) + self.assertEqual(self.b.current_playlist.tracks.get()[5], needle) + self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[5][0] in result) self.assert_(u'OK' in result) - def test_addid_with_empty_uri_does_not_lookup_and_acks(self): - self.b.library.lookup = lambda uri: self.fail("Shouldn't run") + def test_addid_with_empty_uri_acks(self): result = self.h.handle_request(u'addid ""') self.assertEqual(result[0], u'ACK [50@0] {addid} No such song') def test_addid_with_songpos(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'addid "dummy://foo" "3"') - self.assertEqual(len(self.b.current_playlist.tracks), 6) - self.assertEqual(self.b.current_playlist.tracks[3], needle) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[3][0] + self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) + self.assertEqual(self.b.current_playlist.tracks.get()[3], needle) + self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[3][0] in result) self.assert_(u'OK' in result) def test_addid_with_songpos_out_of_bounds_should_ack(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'addid "dummy://foo" "6"') self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index') @@ -85,65 +87,65 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_clear(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'clear') - self.assertEqual(len(self.b.current_playlist.tracks), 0) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 0) + self.assertEqual(self.b.playback.current_track.get(), None) self.assert_(u'OK' in result) def test_delete_songpos(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "%d"' % - self.b.current_playlist.cp_tracks[2][0]) - self.assertEqual(len(self.b.current_playlist.tracks), 4) + self.b.current_playlist.cp_tracks.get()[2][0]) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 4) self.assert_(u'OK' in result) def test_delete_songpos_out_of_bounds(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "5"') - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') def test_delete_open_range(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "1:"') - self.assertEqual(len(self.b.current_playlist.tracks), 1) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 1) self.assert_(u'OK' in result) def test_delete_closed_range(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "1:3"') - self.assertEqual(len(self.b.current_playlist.tracks), 3) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 3) self.assert_(u'OK' in result) def test_delete_range_out_of_bounds(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "5:7"') - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') def test_deleteid(self): self.b.current_playlist.append([Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) result = self.h.handle_request(u'deleteid "1"') - self.assertEqual(len(self.b.current_playlist.tracks), 1) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 1) self.assert_(u'OK' in result) def test_deleteid_does_not_exist(self): self.b.current_playlist.append([Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) result = self.h.handle_request(u'deleteid "12345"') - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song') def test_move_songpos(self): @@ -152,12 +154,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'move "1" "0"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'b') + self.assertEqual(tracks[1].name, 'a') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'e') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_move_open_range(self): @@ -166,12 +169,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'move "2:" "0"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'f') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'b') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'c') + self.assertEqual(tracks[1].name, 'd') + self.assertEqual(tracks[2].name, 'e') + self.assertEqual(tracks[3].name, 'f') + self.assertEqual(tracks[4].name, 'a') + self.assertEqual(tracks[5].name, 'b') self.assert_(u'OK' in result) def test_move_closed_range(self): @@ -180,12 +184,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'move "1:3" "0"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'b') + self.assertEqual(tracks[1].name, 'c') + self.assertEqual(tracks[2].name, 'a') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'e') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_moveid(self): @@ -194,12 +199,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'moveid "4" "2"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'b') + self.assertEqual(tracks[2].name, 'e') + self.assertEqual(tracks[3].name, 'c') + self.assertEqual(tracks[4].name, 'd') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_playlist_returns_same_as_playlistinfo(self): @@ -361,14 +367,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_plchangesposid(self): self.b.current_playlist.append([Track(), Track(), Track()]) result = self.h.handle_request(u'plchangesposid "0"') + cp_tracks = self.b.current_playlist.cp_tracks.get() self.assert_(u'cpos: 0' in result) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[0][0] + self.assert_(u'Id: %d' % cp_tracks[0][0] in result) self.assert_(u'cpos: 2' in result) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[1][0] + self.assert_(u'Id: %d' % cp_tracks[1][0] in result) self.assert_(u'cpos: 2' in result) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[2][0] + self.assert_(u'Id: %d' % cp_tracks[2][0] in result) self.assert_(u'OK' in result) @@ -377,9 +384,9 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - version = self.b.current_playlist.version + version = self.b.current_playlist.version.get() result = self.h.handle_request(u'shuffle') - self.assert_(version < self.b.current_playlist.version) + self.assert_(version < self.b.current_playlist.version.get()) self.assert_(u'OK' in result) def test_shuffle_with_open_range(self): @@ -387,13 +394,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - version = self.b.current_playlist.version + version = self.b.current_playlist.version.get() result = self.h.handle_request(u'shuffle "4:"') - self.assert_(version < self.b.current_playlist.version) - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') + self.assert_(version < self.b.current_playlist.version.get()) + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'b') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') self.assert_(u'OK' in result) def test_shuffle_with_closed_range(self): @@ -401,13 +409,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - version = self.b.current_playlist.version + version = self.b.current_playlist.version.get() result = self.h.handle_request(u'shuffle "1:3"') - self.assert_(version < self.b.current_playlist.version) - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + self.assert_(version < self.b.current_playlist.version.get()) + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'e') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_swap(self): @@ -416,12 +425,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'swap "1" "4"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'e') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'b') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_swapid(self): @@ -430,10 +440,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'swapid "1" "4"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'e') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'b') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 2a2ee4db..77e0ddf0 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -8,8 +8,13 @@ from mopidy.mixers.dummy import DummyMixer class MpdDispatcherTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_register_same_pattern_twice_fails(self): func = lambda: None diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index 486eac4f..fa5634be 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_count(self): result = self.h.handle_request(u'count "tag" "needle"') @@ -65,8 +70,13 @@ class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseFindTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_find_album(self): result = self.h.handle_request(u'find "album" "what"') @@ -117,8 +127,13 @@ class MusicDatabaseFindTest(unittest.TestCase): class MusicDatabaseListTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_list_foo_returns_ack(self): result = self.h.handle_request(u'list "foo"') @@ -308,8 +323,13 @@ class MusicDatabaseListTest(unittest.TestCase): class MusicDatabaseSearchTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_search_album(self): result = self.h.handle_request(u'search "album" "analbum"') diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 8f0560c7..8601aa9c 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -1,5 +1,6 @@ import unittest +from mopidy.backends.base import PlaybackController from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer @@ -7,29 +8,38 @@ from mopidy.models import Track from tests import SkipTest +PAUSED = PlaybackController.PAUSED +PLAYING = PlaybackController.PLAYING +STOPPED = PlaybackController.STOPPED + class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_consume_off(self): result = self.h.handle_request(u'consume "0"') - self.assertFalse(self.b.playback.consume) + self.assertFalse(self.b.playback.consume.get()) self.assert_(u'OK' in result) def test_consume_off_without_quotes(self): result = self.h.handle_request(u'consume 0') - self.assertFalse(self.b.playback.consume) + self.assertFalse(self.b.playback.consume.get()) self.assert_(u'OK' in result) def test_consume_on(self): result = self.h.handle_request(u'consume "1"') - self.assertTrue(self.b.playback.consume) + self.assertTrue(self.b.playback.consume.get()) self.assert_(u'OK' in result) def test_consume_on_without_quotes(self): result = self.h.handle_request(u'consume 1') - self.assertTrue(self.b.playback.consume) + self.assertTrue(self.b.playback.consume.get()) self.assert_(u'OK' in result) def test_crossfade(self): @@ -38,97 +48,97 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): def test_random_off(self): result = self.h.handle_request(u'random "0"') - self.assertFalse(self.b.playback.random) + self.assertFalse(self.b.playback.random.get()) self.assert_(u'OK' in result) def test_random_off_without_quotes(self): result = self.h.handle_request(u'random 0') - self.assertFalse(self.b.playback.random) + self.assertFalse(self.b.playback.random.get()) self.assert_(u'OK' in result) def test_random_on(self): result = self.h.handle_request(u'random "1"') - self.assertTrue(self.b.playback.random) + self.assertTrue(self.b.playback.random.get()) self.assert_(u'OK' in result) def test_random_on_without_quotes(self): result = self.h.handle_request(u'random 1') - self.assertTrue(self.b.playback.random) + self.assertTrue(self.b.playback.random.get()) self.assert_(u'OK' in result) def test_repeat_off(self): result = self.h.handle_request(u'repeat "0"') - self.assertFalse(self.b.playback.repeat) + self.assertFalse(self.b.playback.repeat.get()) self.assert_(u'OK' in result) def test_repeat_off_without_quotes(self): result = self.h.handle_request(u'repeat 0') - self.assertFalse(self.b.playback.repeat) + self.assertFalse(self.b.playback.repeat.get()) self.assert_(u'OK' in result) def test_repeat_on(self): result = self.h.handle_request(u'repeat "1"') - self.assertTrue(self.b.playback.repeat) + self.assertTrue(self.b.playback.repeat.get()) self.assert_(u'OK' in result) def test_repeat_on_without_quotes(self): result = self.h.handle_request(u'repeat 1') - self.assertTrue(self.b.playback.repeat) + self.assertTrue(self.b.playback.repeat.get()) self.assert_(u'OK' in result) def test_setvol_below_min(self): result = self.h.handle_request(u'setvol "-10"') self.assert_(u'OK' in result) - self.assertEqual(0, self.b.mixer.volume) + self.assertEqual(0, self.mixer.volume.get()) def test_setvol_min(self): result = self.h.handle_request(u'setvol "0"') self.assert_(u'OK' in result) - self.assertEqual(0, self.b.mixer.volume) + self.assertEqual(0, self.mixer.volume.get()) def test_setvol_middle(self): result = self.h.handle_request(u'setvol "50"') self.assert_(u'OK' in result) - self.assertEqual(50, self.b.mixer.volume) + self.assertEqual(50, self.mixer.volume.get()) def test_setvol_max(self): result = self.h.handle_request(u'setvol "100"') self.assert_(u'OK' in result) - self.assertEqual(100, self.b.mixer.volume) + self.assertEqual(100, self.mixer.volume.get()) def test_setvol_above_max(self): result = self.h.handle_request(u'setvol "110"') self.assert_(u'OK' in result) - self.assertEqual(100, self.b.mixer.volume) + self.assertEqual(100, self.mixer.volume.get()) def test_setvol_plus_is_ignored(self): result = self.h.handle_request(u'setvol "+10"') self.assert_(u'OK' in result) - self.assertEqual(10, self.b.mixer.volume) + self.assertEqual(10, self.mixer.volume.get()) def test_setvol_without_quotes(self): result = self.h.handle_request(u'setvol 50') self.assert_(u'OK' in result) - self.assertEqual(50, self.b.mixer.volume) + self.assertEqual(50, self.mixer.volume.get()) def test_single_off(self): result = self.h.handle_request(u'single "0"') - self.assertFalse(self.b.playback.single) + self.assertFalse(self.b.playback.single.get()) self.assert_(u'OK' in result) def test_single_off_without_quotes(self): result = self.h.handle_request(u'single 0') - self.assertFalse(self.b.playback.single) + self.assertFalse(self.b.playback.single.get()) self.assert_(u'OK' in result) def test_single_on(self): result = self.h.handle_request(u'single "1"') - self.assertTrue(self.b.playback.single) + self.assertTrue(self.b.playback.single.get()) self.assert_(u'OK' in result) def test_single_on_without_quotes(self): result = self.h.handle_request(u'single 1') - self.assertTrue(self.b.playback.single) + self.assertTrue(self.b.playback.single.get()) self.assert_(u'OK' in result) def test_replay_gain_mode_off(self): @@ -176,8 +186,13 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_next(self): result = self.h.handle_request(u'next') @@ -189,155 +204,155 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.h.handle_request(u'pause "1"') result = self.h.handle_request(u'pause "0"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_pause_on(self): self.b.current_playlist.append([Track()]) self.h.handle_request(u'play "0"') result = self.h.handle_request(u'pause "1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PAUSED, self.b.playback.state) + self.assertEqual(PAUSED, self.b.playback.state.get()) def test_pause_toggle(self): self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'play "0"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) result = self.h.handle_request(u'pause') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PAUSED, self.b.playback.state) + self.assertEqual(PAUSED, self.b.playback.state.get()) result = self.h.handle_request(u'pause') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_without_pos(self): self.b.current_playlist.append([Track()]) - self.b.playback.state = self.b.playback.PAUSED + self.b.playback.state = PAUSED result = self.h.handle_request(u'play') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_with_pos(self): self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'play "0"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_with_pos_without_quotes(self): self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'play 0') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_with_pos_out_of_bounds(self): self.b.current_playlist.append([]) result = self.h.handle_request(u'play "0"') self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index') - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) + self.assertEqual(STOPPED, self.b.playback.state.get()) def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'a') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'a') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.playback.play() self.b.playback.next() self.b.playback.stop() - self.assertNotEqual(self.b.playback.current_track, None) + self.assertNotEqual(self.b.playback.current_track.get(), None) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'b') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'b') def test_play_minus_one_on_empty_playlist_does_not_ack(self): self.b.current_playlist.clear() result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(STOPPED, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get(), None) def test_play_minus_is_ignored_if_playing(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_play_minus_one_resumes_if_paused(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) self.b.playback.pause() - self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) + self.assertEquals(PAUSED, self.b.playback.state.get()) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_playid(self): self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'playid "0"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self): - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'a') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'a') def test_playid_minus_one_plays_current_track_if_current_track_is_set(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.playback.play() self.b.playback.next() self.b.playback.stop() - self.assertNotEqual(self.b.playback.current_track, None) + self.assertNotEqual(self.b.playback.current_track.get(), None) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'b') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'b') def test_playid_minus_one_on_empty_playlist_does_not_ack(self): self.b.current_playlist.clear() result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(STOPPED, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get(), None) def test_playid_minus_is_ignored_if_playing(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_playid_minus_one_resumes_if_paused(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) self.b.playback.pause() - self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) + self.assertEquals(PAUSED, self.b.playback.state.get()) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_playid_which_does_not_exist(self): self.b.current_playlist.append([Track()]) @@ -361,20 +376,20 @@ class PlaybackControlHandlerTest(unittest.TestCase): [Track(uri='1', length=40000), seek_track]) result = self.h.handle_request(u'seek "1" "30"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.current_track, seek_track) + self.assertEqual(self.b.playback.current_track.get(), seek_track) def test_seek_without_quotes(self): self.b.current_playlist.append([Track(length=40000)]) self.h.handle_request(u'seek 0') result = self.h.handle_request(u'seek 0 30') self.assert_(u'OK' in result) - self.assert_(self.b.playback.time_position >= 30000) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_seekid(self): self.b.current_playlist.append([Track(length=40000)]) result = self.h.handle_request(u'seekid "0" "30"') self.assert_(u'OK' in result) - self.assert_(self.b.playback.time_position >= 30000) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_seekid_with_cpid(self): seek_track = Track(uri='2', length=40000) @@ -382,10 +397,10 @@ class PlaybackControlHandlerTest(unittest.TestCase): [Track(length=40000), seek_track]) result = self.h.handle_request(u'seekid "1" "30"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.current_cpid, 1) - self.assertEqual(self.b.playback.current_track, seek_track) + self.assertEqual(self.b.playback.current_cpid.get(), 1) + self.assertEqual(self.b.playback.current_track.get(), seek_track) def test_stop(self): result = self.h.handle_request(u'stop') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) + self.assertEqual(STOPPED, self.b.playback.state.get()) diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py index a4491d75..be95c49b 100644 --- a/tests/frontends/mpd/reflection_test.py +++ b/tests/frontends/mpd/reflection_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class ReflectionHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_commands_returns_list_of_all_commands(self): result = self.h.handle_request(u'commands') diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py index 7e7163d8..1d661b5a 100644 --- a/tests/frontends/mpd/regression_test.py +++ b/tests/frontends/mpd/regression_test.py @@ -18,26 +18,31 @@ class IssueGH17RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend.start().proxy() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), None, Track(uri='d'), Track(uri='e'), Track(uri='f')]) - self.mpd = dispatcher.MpdDispatcher(backend=self.backend) + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test(self): random.seed(1) # Playlist order: abcfde self.mpd.handle_request(u'play') - self.assertEquals('a', self.backend.playback.current_track.uri) + self.assertEquals('a', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'random "1"') self.mpd.handle_request(u'next') - self.assertEquals('b', self.backend.playback.current_track.uri) + self.assertEquals('b', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'next') # Should now be at track 'c', but playback fails and it skips ahead - self.assertEquals('f', self.backend.playback.current_track.uri) + self.assertEquals('f', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'next') - self.assertEquals('d', self.backend.playback.current_track.uri) + self.assertEquals('d', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'next') - self.assertEquals('e', self.backend.playback.current_track.uri) + self.assertEquals('e', self.backend.playback.current_track.get().uri) class IssueGH18RegressionTest(unittest.TestCase): @@ -52,11 +57,16 @@ class IssueGH18RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend.start().proxy() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) - self.mpd = dispatcher.MpdDispatcher(backend=self.backend) + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test(self): random.seed(1) @@ -67,11 +77,11 @@ class IssueGH18RegressionTest(unittest.TestCase): self.mpd.handle_request(u'next') self.mpd.handle_request(u'next') - cp_track_1 = self.backend.playback.current_cp_track + cp_track_1 = self.backend.playback.current_cp_track.get() self.mpd.handle_request(u'next') - cp_track_2 = self.backend.playback.current_cp_track + cp_track_2 = self.backend.playback.current_cp_track.get() self.mpd.handle_request(u'next') - cp_track_3 = self.backend.playback.current_cp_track + cp_track_3 = self.backend.playback.current_cp_track.get() self.assertNotEqual(cp_track_1, cp_track_2) self.assertNotEqual(cp_track_2, cp_track_3) @@ -91,11 +101,16 @@ class IssueGH22RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend.start().proxy() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) - self.mpd = dispatcher.MpdDispatcher(backend=self.backend) + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test(self): random.seed(1) diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index 48c7e790..ef963347 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -1,11 +1,19 @@ import unittest from mopidy import settings +from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import server +from mopidy.mixers.dummy import DummyMixer class MpdServerTest(unittest.TestCase): def setUp(self): - self.server = server.MpdServer(None) + self.backend = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.server = server.MpdServer() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): server.socket.has_ipv6 = True @@ -20,9 +28,13 @@ class MpdServerTest(unittest.TestCase): class MpdSessionTest(unittest.TestCase): def setUp(self): - self.session = server.MpdSession(None, None, (None, None), None) + self.backend = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.session = server.MpdSession(None, None, (None, None)) def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() settings.runtime.clear() def test_found_terminator_catches_decode_error(self): diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 1afe6ccd..791d734f 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,14 +1,24 @@ import unittest +from mopidy.backends.base import PlaybackController from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track +PAUSED = PlaybackController.PAUSED +PLAYING = PlaybackController.PLAYING +STOPPED = PlaybackController.STOPPED + class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_clearerror(self): result = self.h.handle_request(u'clearerror') @@ -77,7 +87,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(int(result['volume']), 0) def test_status_method_contains_volume(self): - self.b.mixer.volume = 17 + self.mixer.volume = 17 result = dict(dispatcher.status.status(self.h)) self.assert_('volume' in result) self.assertEqual(int(result['volume']), 17) @@ -136,20 +146,20 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(int(result['xfade']) >= 0) def test_status_method_contains_state_is_play(self): - self.b.playback.state = self.b.playback.PLAYING + self.b.playback.state = PLAYING result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'play') def test_status_method_contains_state_is_stop(self): - self.b.playback.state = self.b.playback.STOPPED + self.b.playback.state = STOPPED result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'stop') def test_status_method_contains_state_is_pause(self): - self.b.playback.state = self.b.playback.PLAYING - self.b.playback.state = self.b.playback.PAUSED + self.b.playback.state = PLAYING + self.b.playback.state = PAUSED result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'pause') @@ -189,8 +199,8 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(position <= total) def test_status_method_when_playing_contains_elapsed(self): - self.b.playback.state = self.b.playback.PAUSED - self.b.playback._play_time_accumulated = 59123 + self.b.playback.state = PAUSED + self.b.playback.play_time_accumulated = 59123 result = dict(dispatcher.status.status(self.h)) self.assert_('elapsed' in result) self.assertEqual(int(result['elapsed']), 59123) diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py index 5b66d723..83d43792 100644 --- a/tests/frontends/mpd/stickers_test.py +++ b/tests/frontends/mpd/stickers_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class StickersHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_sticker_get(self): result = self.h.handle_request( diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index a24cbb88..e981c9ed 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -8,8 +8,13 @@ from mopidy.models import Track, Playlist class StoredPlaylistsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_listplaylist(self): self.b.stored_playlists.playlists = [ @@ -49,22 +54,23 @@ class StoredPlaylistsHandlerTest(unittest.TestCase): def test_load_known_playlist_appends_to_current_playlist(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.b.stored_playlists.playlists = [Playlist(name='A-list', tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])] result = self.h.handle_request(u'load "A-list"') self.assert_(u'OK' in result) - self.assertEqual(len(self.b.current_playlist.tracks), 5) - self.assertEqual(self.b.current_playlist.tracks[0].uri, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].uri, 'b') - self.assertEqual(self.b.current_playlist.tracks[2].uri, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].uri, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].uri, 'e') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(len(tracks), 5) + self.assertEqual(tracks[0].uri, 'a') + self.assertEqual(tracks[1].uri, 'b') + self.assertEqual(tracks[2].uri, 'c') + self.assertEqual(tracks[3].uri, 'd') + self.assertEqual(tracks[4].uri, 'e') def test_load_unknown_playlist_acks(self): result = self.h.handle_request(u'load "unknown playlist"') self.assert_(u'ACK [50@0] {load} No such playlist' in result) - self.assertEqual(len(self.b.current_playlist.tracks), 0) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 0) def test_playlistadd(self): result = self.h.handle_request( diff --git a/tests/mixers/base_test.py b/tests/mixers/base_test.py index 395d8f7b..54cd8773 100644 --- a/tests/mixers/base_test.py +++ b/tests/mixers/base_test.py @@ -11,7 +11,7 @@ class BaseMixerTest(object): assert self.mixer_class is not None, \ "mixer_class must be set in subclass" # pylint: disable = E1102 - self.mixer = self.mixer_class(None) + self.mixer = self.mixer_class() # pylint: enable = E1102 def test_initial_volume(self): diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index b4cb5ef9..d2e3c263 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -18,12 +18,10 @@ class GStreamerOutputTest(unittest.TestCase): def setUp(self): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) self.song_uri = path_to_uri(data_folder('song1.wav')) - self.core_queue = multiprocessing.Queue() - self.output = GStreamerOutput(self.core_queue) - self.output.start() + self.output = GStreamerOutput() + self.output.on_start() def tearDown(self): - self.output.destroy() settings.runtime.clear() def test_play_uri_existing_file(self):