Merge branch 'feature/pykka-actors' into develop

This commit is contained in:
Stein Magnus Jodal 2011-04-03 12:29:13 +02:00
commit be1e0fa819
64 changed files with 847 additions and 1060 deletions

View File

@ -1,5 +1,5 @@
#! /usr/bin/env python
if __name__ == '__main__':
from mopidy.__main__ import main
from mopidy.core import main
main()

View File

@ -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::

View File

@ -25,6 +25,8 @@ Otherwise, make sure you got the required dependencies installed.
- Python >= 2.6, < 3
- `Pykka <http://jodal.github.com/pykka/>`_ >= 0.12
- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`.
- Mixer dependencies: The default mixer does not require any additional

View File

@ -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):

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 = []

View File

@ -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]

View File

@ -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):

View File

@ -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 <http://www.spotify.com/>`_
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

View File

@ -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=[])

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
<http://www.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])

View File

@ -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()

View File

@ -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

View File

@ -34,6 +34,6 @@ def outputs(frontend):
"""
return [
('outputid', 0),
('outputname', frontend.backend.__class__.__name__),
('outputname', None),
('outputenabled', 1),
]

View File

@ -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<uri>[^"]*)"$')
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<start>\d+):(?P<end>\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<songpos>-?\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<tag>[^"]+)" "(?P<needle>[^"]+)"$')
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
@ -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<version>\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)

View File

@ -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<query>("?([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<uri>[^"]+)")*$')
def update(frontend, uri=None, rescan_unmodified_files=False):

View File

@ -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<state>[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<cpid>\d+)"$')
@handle_pattern(r'^playid "(?P<cpid>-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<state>[01])$')
@handle_pattern(r'^random "(?P<state>[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<state>[01])$')
@handle_pattern(r'^single "(?P<state>[01])"$')

View File

@ -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()]

View File

@ -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<subsystems>.+)$')
@ -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

View File

@ -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')

View File

@ -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."""

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -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):

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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 <http://gstreamer.freedesktop.org/>`_.
**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

View File

@ -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('')

View File

@ -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

1
requirements/core.txt Normal file
View File

@ -0,0 +1 @@
Pykka >= 0.12

View File

@ -1,3 +1,4 @@
coverage
mock
nose
tox

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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"')

View File

@ -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())

View File

@ -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')

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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(

View File

@ -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(

View File

@ -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):

View File

@ -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):