Merge branch 'develop' into gstreamer-local-backend

This commit is contained in:
Stein Magnus Jodal 2010-08-21 23:08:37 +02:00
commit 7062e3c8ee
49 changed files with 589 additions and 450 deletions

View File

@ -4,6 +4,8 @@
.. automodule:: mopidy.frontends.mpd
:synopsis: MPD frontend
:members:
:undoc-members:
MPD server
@ -17,10 +19,21 @@ MPD server
.. inheritance-diagram:: mopidy.frontends.mpd.server
MPD frontend
============
MPD session
===========
.. automodule:: mopidy.frontends.mpd.frontend
.. automodule:: mopidy.frontends.mpd.session
:synopsis: MPD client session
:members:
:undoc-members:
.. inheritance-diagram:: mopidy.frontends.mpd.session
MPD dispatcher
==============
.. automodule:: mopidy.frontends.mpd.dispatcher
:synopsis: MPD request dispatcher
:members:
:undoc-members:

View File

@ -39,8 +39,6 @@ greatly improved MPD client support.
the packages created by ``setup.py`` for i.e. PyPI.
- MPD frontend:
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
- Split gigantic protocol implementation into eleven modules.
- Search improvements, including support for multi-word search.
- Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty
or when a current track is set.
@ -56,8 +54,14 @@ greatly improved MPD client support.
- Fix ``load`` so that one can append a playlist to the current playlist, and
make it return the correct error message if the playlist is not found.
- Support for single track repeat added. (Fixes: :issue:`4`)
- Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`.
- Split gigantic protocol implementation into eleven modules.
- Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming
in backends.
- Remove setting :attr:`mopidy.settings.SERVER` and
:attr:`mopidy.settings.FRONTEND` in favour of the new
:attr:`mopidy.settings.FRONTENDS`.
- Run MPD server in its own process.
- Backends:

View File

@ -130,7 +130,7 @@ html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.

View File

@ -12,40 +12,94 @@ In addition to what you'll find here, don't forget the :doc:`/api/index`.
Class instantiation and usage
=============================
The following diagram shows how Mopidy with the despotify backend and ALSA
mixer is wired together. The gray nodes are part of external dependencies, and
not Mopidy. The red nodes lives in the ``main`` process (running an
:mod:`asyncore` loop), while the blue nodes lives in a secondary process named
``core`` (running a service loop in :class:`mopidy.core.CoreProcess`).
The following diagram shows how Mopidy is wired together with the MPD client,
the Spotify service, and the speakers.
**Legend**
- Filled red boxes are the key external systems.
- Gray boxes are external dependencies.
- Blue circles lives in the ``main`` process, also known as ``CoreProcess``.
It processing messages on the core queue.
- Purple circles lives in a process named ``MpdProcess``, running an
:mod:`asyncore` loop.
- Green circles lives in a process named ``GStreamerProcess``.
- Brown circle is a thread living in the ``CoreProcess``.
.. digraph:: class_instantiation_and_usage
"spytify" [ color="gray" ]
"despotify" [ color="gray" ]
"alsaaudio" [ color="gray" ]
"__main__" [ color="red" ]
"main" [ color="blue" ]
"CoreProcess" [ color="blue" ]
"DespotifyBackend" [ color="blue" ]
"AlsaMixer" [ color="blue" ]
# Frontend
"MPD client" [ color="red", style="filled", shape="box" ]
"MpdFrontend" [ color="blue" ]
"MpdServer" [ color="red" ]
"MpdSession" [ color="red" ]
"__main__" -> "CoreProcess" [ label="create" ]
"__main__" -> "MpdServer" [ label="create" ]
"CoreProcess" -> "DespotifyBackend" [ label="create" ]
"MpdProcess" [ color="purple" ]
"MpdServer" [ color="purple" ]
"MpdSession" [ color="purple" ]
"MpdDispatcher" [ color="blue" ]
# Backend
"Libspotify\nBackend" [ color="blue" ]
"Libspotify\nSessionManager" [ color="brown" ]
"pyspotify" [ color="gray", shape="box" ]
"libspotify" [ color="gray", shape="box" ]
"Spotify" [ color="red", style="filled", shape="box" ]
# Output/mixer
"GStreamer\nOutput" [ color="blue" ]
"GStreamer\nSoftwareMixer" [ color="blue" ]
"GStreamer\nProcess" [ color="green" ]
"GStreamer" [ color="gray", shape="box" ]
"Speakers" [ color="red", style="filled", shape="box" ]
"main" -> "CoreProcess" [ label="create" ]
# Frontend
"CoreProcess" -> "MpdFrontend" [ label="create" ]
"MpdServer" -> "MpdSession" [ label="create one per client" ]
"MpdSession" -> "MpdFrontend" [ label="pass MPD requests to" ]
"MpdFrontend" -> "DespotifyBackend" [ label="use backend API" ]
"DespotifyBackend" -> "AlsaMixer" [ label="create and use mixer API" ]
"DespotifyBackend" -> "spytify" [ label="use Python wrapper" ]
"spytify" -> "despotify" [ label="use C library" ]
"AlsaMixer" -> "alsaaudio" [ label="use Python library" ]
"MpdFrontend" -> "MpdProcess" [ label="create" ]
"MpdFrontend" -> "MpdDispatcher" [ label="create" ]
"MpdProcess" -> "MpdServer" [ label="create" ]
"MpdServer" -> "MpdSession" [ label="create one\nper client" ]
"MpdSession" -> "MpdDispatcher" [
label="pass requests\nvia core_queue" ]
"MpdDispatcher" -> "MpdSession" [
label="pass response\nvia reply_to pipe" ]
"MpdDispatcher" -> "Libspotify\nBackend" [ label="use backend API" ]
"MPD client" -> "MpdServer" [ label="connect" ]
"MPD client" -> "MpdSession" [ label="request" ]
"MpdSession" -> "MPD client" [ label="response" ]
# Backend
"CoreProcess" -> "Libspotify\nBackend" [ label="create" ]
"Libspotify\nBackend" -> "Libspotify\nSessionManager" [
label="creates and uses" ]
"Libspotify\nSessionManager" -> "Libspotify\nBackend" [
label="pass commands\nvia core_queue" ]
"Libspotify\nSessionManager" -> "pyspotify" [ label="use Python\nwrapper" ]
"pyspotify" -> "Libspotify\nSessionManager" [ label="use callbacks" ]
"pyspotify" -> "libspotify" [ label="use C library" ]
"libspotify" -> "Spotify" [ label="use service" ]
"Libspotify\nSessionManager" -> "GStreamer\nProcess" [
label="pass commands\nand audio data\nvia output_queue" ]
# Output/mixer
"Libspotify\nBackend" -> "GStreamer\nSoftwareMixer" [
label="create and\nuse mixer API" ]
"GStreamer\nSoftwareMixer" -> "GStreamer\nProcess" [
label="pass commands\nvia output_queue" ]
"CoreProcess" -> "GStreamer\nOutput" [ label="create" ]
"GStreamer\nOutput" -> "GStreamer\nProcess" [ label="create" ]
"GStreamer\nProcess" -> "GStreamer" [ label="use library" ]
"GStreamer" -> "Speakers" [ label="play audio" ]
Thread/process communication
============================
.. warning::
This section is currently outdated.
- Everything starts with ``Main``.
- ``Main`` creates a ``Core`` process which runs the frontend, backend, and
mixer.

View File

@ -5,9 +5,6 @@ if not (2, 6) <= sys.version_info < (3,):
def get_version():
return u'0.1.0a4'
def get_mpd_protocol_version():
return u'0.16.0'
class MopidyException(Exception):
def __init__(self, message, *args, **kwargs):
super(MopidyException, self).__init__(message, *args, **kwargs)

View File

@ -1,85 +1,17 @@
import asyncore
import logging
import logging.handlers
import multiprocessing
import optparse
import os
import sys
# Add ../ to the path so we can run Mopidy from a Git checkout without
# installing it on the system.
sys.path.insert(0,
os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
from mopidy import get_version, settings, SettingsError
from mopidy.process import CoreProcess
from mopidy.utils import get_class
from mopidy.utils.path import get_or_create_folder
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.main')
from mopidy.core import CoreProcess
def main():
options = _parse_options()
_setup_logging(options.verbosity_level, options.dump)
settings.validate()
logger.info('-- Starting Mopidy --')
get_or_create_folder('~/.mopidy/')
core_queue = multiprocessing.Queue()
get_class(settings.SERVER)(core_queue).start()
output_class = get_class(settings.OUTPUT)
backend_class = get_class(settings.BACKENDS[0])
frontend_class = get_class(settings.FRONTEND)
core = CoreProcess(core_queue, output_class, backend_class, frontend_class)
core.start()
asyncore.loop()
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('--dump',
action='store_true', dest='dump',
help='dump debug log to file')
parser.add_option('--list-settings',
action='callback', callback=list_settings_optparse_callback,
help='list current settings')
return parser.parse_args()[0]
def _setup_logging(verbosity_level, dump):
_setup_console_logging(verbosity_level)
if dump:
_setup_dump_logging()
def _setup_console_logging(verbosity_level):
if verbosity_level == 0:
level = logging.WARNING
elif verbosity_level == 2:
level = logging.DEBUG
else:
level = logging.INFO
logging.basicConfig(format=settings.CONSOLE_LOG_FORMAT, level=level)
def _setup_dump_logging():
root = logging.getLogger('')
root.setLevel(logging.DEBUG)
formatter = logging.Formatter(settings.DUMP_LOG_FORMAT)
handler = logging.handlers.RotatingFileHandler(
settings.DUMP_LOG_FILENAME, maxBytes=102400, backupCount=3)
handler.setFormatter(formatter)
root.addHandler(handler)
# Explictly call run() instead of start(), since we don't need to start
# another process.
CoreProcess().run()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
logger.info(u'Interrupted by user')
sys.exit(0)
except SettingsError, e:
logger.error(e)
sys.exit(1)
except SystemExit, e:
logger.error(e)
sys.exit(1)
main()

View File

@ -4,7 +4,7 @@ import multiprocessing
from spotify import Link, SpotifyError
from mopidy.backends.base import BasePlaybackController
from mopidy.process import pickle_connection
from mopidy.utils.process import pickle_connection
logger = logging.getLogger('mopidy.backends.libspotify.playback')

View File

@ -18,7 +18,10 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread):
def __init__(self, username, password, core_queue, output_queue):
SpotifySessionManager.__init__(self, username, password)
threading.Thread.__init__(self)
threading.Thread.__init__(self, name='LibspotifySessionManagerThread')
# Run as a daemon thread, so Mopidy won't wait for this thread to exit
# before Mopidy exits.
self.daemon = True
self.core_queue = core_queue
self.output_queue = output_queue
self.connected = threading.Event()

88
mopidy/core.py Normal file
View File

@ -0,0 +1,88 @@
import logging
import multiprocessing
import optparse
from mopidy import get_version, settings
from mopidy.utils import get_class
from mopidy.utils.log import setup_logging
from mopidy.utils.path import get_or_create_folder
from mopidy.utils.process import BaseProcess, unpickle_connection
from mopidy.utils.settings import list_settings_optparse_callback
logger = logging.getLogger('mopidy.core')
class CoreProcess(BaseProcess):
def __init__(self):
super(CoreProcess, self).__init__(name='CoreProcess')
self.core_queue = multiprocessing.Queue()
self.options = self.parse_options()
self.output_queue = None
self.backend = None
self.frontend = None
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('--dump',
action='store_true', dest='dump',
help='dump debug log to file')
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):
logger.info(u'-- Starting Mopidy --')
self.setup()
while True:
message = self.core_queue.get()
self.process_message(message)
def setup(self):
self.setup_logging()
self.setup_settings()
self.output_queue = self.setup_output(self.core_queue)
self.backend = self.setup_backend(self.core_queue, self.output_queue)
self.frontend = self.setup_frontend(self.core_queue, self.backend)
def setup_logging(self):
setup_logging(self.options.verbosity_level, self.options.dump)
def setup_settings(self):
get_or_create_folder('~/.mopidy/')
settings.validate()
def setup_output(self, core_queue):
output_queue = multiprocessing.Queue()
get_class(settings.OUTPUT)(core_queue, output_queue)
return output_queue
def setup_backend(self, core_queue, output_queue):
return get_class(settings.BACKENDS[0])(core_queue, output_queue)
def setup_frontend(self, core_queue, backend):
frontend = get_class(settings.FRONTENDS[0])()
frontend.start_server(core_queue)
frontend.create_dispatcher(backend)
return frontend
def process_message(self, message):
if message.get('to') == 'output':
self.output_queue.put(message)
elif message['command'] == 'mpd_request':
response = self.frontend.dispatcher.handle_request(message['request'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
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)

View File

@ -1,93 +1,32 @@
import re
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
from mopidy.frontends.mpd.process import MpdProcess
from mopidy import MopidyException
class MpdAckError(MopidyException):
class MpdFrontend(object):
"""
Available MPD error codes::
ACK_ERROR_NOT_LIST = 1
ACK_ERROR_ARG = 2
ACK_ERROR_PASSWORD = 3
ACK_ERROR_PERMISSION = 4
ACK_ERROR_UNKNOWN = 5
ACK_ERROR_NO_EXIST = 50
ACK_ERROR_PLAYLIST_MAX = 51
ACK_ERROR_SYSTEM = 52
ACK_ERROR_PLAYLIST_LOAD = 53
ACK_ERROR_UPDATE_ALREADY = 54
ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56
The MPD frontend.
"""
def __init__(self, message=u'', error_code=0, index=0, command=u''):
super(MpdAckError, self).__init__(message, error_code, index, command)
self.message = message
self.error_code = error_code
self.index = index
self.command = command
def __init__(self):
self.process = None
self.dispatcher = None
def get_mpd_ack(self):
def start_server(self, core_queue):
"""
MPD error code format::
Starts the MPD server.
ACK [%(error_code)i@%(index)i] {%(command)s} description
:param core_queue: the core queue
:type core_queue: :class:`multiprocessing.Queue`
"""
return u'ACK [%i@%i] {%s} %s' % (
self.error_code, self.index, self.command, self.message)
self.process = MpdProcess(core_queue)
self.process.start()
class MpdArgError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdArgError, self).__init__(*args, **kwargs)
self.error_code = 2 # ACK_ERROR_ARG
def create_dispatcher(self, backend):
"""
Creates a dispatcher for MPD requests.
class MpdUnknownCommand(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
self.message = u'unknown command "%s"' % self.command
self.command = u''
self.error_code = 5 # ACK_ERROR_UNKNOWN
class MpdNoExistError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdNoExistError, self).__init__(*args, **kwargs)
self.error_code = 50 # ACK_ERROR_NO_EXIST
class MpdNotImplemented(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdNotImplemented, self).__init__(*args, **kwargs)
self.message = u'Not implemented'
mpd_commands = set()
request_handlers = {}
def handle_pattern(pattern):
"""
Decorator for connecting command handlers to command patterns.
If you use named groups in the pattern, the decorated method will get the
groups as keyword arguments. If the group is optional, remember to give the
argument a default value.
For example, if the command is ``do that thing`` the ``what`` argument will
be ``this thing``::
@handle_pattern('^do (?P<what>.+)$')
def do(what):
...
:param pattern: regexp pattern for matching commands
:type pattern: string
"""
def decorator(func):
match = re.search('([a-z_]+)', pattern)
if match is not None:
mpd_commands.add(match.group())
if pattern in request_handlers:
raise ValueError(u'Tried to redefine handler for %s with %s' % (
pattern, func))
request_handlers[pattern] = func
func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % (
pattern, func.__doc__ or '')
return func
return decorator
:param backend: the backend
:type backend: :class:`mopidy.backends.base.BaseBackend`
:rtype: :class:`mopidy.frontends.mpd.dispatcher.MpdDispatcher`
"""
self.dispatcher = MpdDispatcher(backend)
return self.dispatcher

View File

@ -1,20 +1,18 @@
import logging
import re
from mopidy.frontends.mpd import (mpd_commands, request_handlers,
handle_pattern, MpdAckError, MpdArgError, MpdUnknownCommand)
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
MpdUnknownCommand)
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
# Do not remove the following import. The protocol modules must be imported to
# get them registered as request handlers.
from mopidy.frontends.mpd.protocol import (audio_output, command_list,
connection, current_playlist, music_db, playback, reflection, status,
stickers, stored_playlists)
connection, current_playlist, empty, music_db, playback, reflection,
status, stickers, stored_playlists)
from mopidy.utils import flatten
logger = logging.getLogger('mopidy.frontends.mpd.frontend')
class MpdFrontend(object):
class MpdDispatcher(object):
"""
The MPD frontend dispatches MPD requests to the correct handler.
Dispatches MPD requests to the correct handler.
"""
def __init__(self, backend=None):
@ -72,8 +70,3 @@ class MpdFrontend(object):
if add_ok and (not response or not response[-1].startswith(u'ACK')):
response.append(u'OK')
return response
@handle_pattern(r'^$')
def empty(frontend):
"""The original MPD server returns ``OK`` on an empty request."""
pass

View File

@ -0,0 +1,57 @@
from mopidy import MopidyException
class MpdAckError(MopidyException):
"""
Available MPD error codes::
ACK_ERROR_NOT_LIST = 1
ACK_ERROR_ARG = 2
ACK_ERROR_PASSWORD = 3
ACK_ERROR_PERMISSION = 4
ACK_ERROR_UNKNOWN = 5
ACK_ERROR_NO_EXIST = 50
ACK_ERROR_PLAYLIST_MAX = 51
ACK_ERROR_SYSTEM = 52
ACK_ERROR_PLAYLIST_LOAD = 53
ACK_ERROR_UPDATE_ALREADY = 54
ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56
"""
def __init__(self, message=u'', error_code=0, index=0, command=u''):
super(MpdAckError, self).__init__(message, error_code, index, command)
self.message = message
self.error_code = error_code
self.index = index
self.command = command
def get_mpd_ack(self):
"""
MPD error code format::
ACK [%(error_code)i@%(index)i] {%(command)s} description
"""
return u'ACK [%i@%i] {%s} %s' % (
self.error_code, self.index, self.command, self.message)
class MpdArgError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdArgError, self).__init__(*args, **kwargs)
self.error_code = 2 # ACK_ERROR_ARG
class MpdUnknownCommand(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
self.message = u'unknown command "%s"' % self.command
self.command = u''
self.error_code = 5 # ACK_ERROR_UNKNOWN
class MpdNoExistError(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdNoExistError, self).__init__(*args, **kwargs)
self.error_code = 50 # ACK_ERROR_NO_EXIST
class MpdNotImplemented(MpdAckError):
def __init__(self, *args, **kwargs):
super(MpdNotImplemented, self).__init__(*args, **kwargs)
self.message = u'Not implemented'

View File

@ -0,0 +1,18 @@
import asyncore
import logging
from mopidy.frontends.mpd.server import MpdServer
from mopidy.utils.process import BaseProcess
logger = logging.getLogger('mopidy.frontends.mpd.process')
class MpdProcess(BaseProcess):
def __init__(self, core_queue):
super(MpdProcess, self).__init__(name='MpdProcess')
self.core_queue = core_queue
def run_inside_try(self):
logger.debug(u'Starting MPD server process')
server = MpdServer(self.core_queue)
server.start()
asyncore.loop()

View File

@ -10,8 +10,47 @@ implement our own MPD server which is compatible with the numerous existing
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
"""
import re
#: The MPD protocol uses UTF-8 for encoding all data.
ENCODING = u'utf-8'
#: The MPD protocol uses ``\n`` as line terminator.
LINE_TERMINATOR = u'\n'
#: The MPD protocol version is 0.16.0.
VERSION = u'0.16.0'
mpd_commands = set()
request_handlers = {}
def handle_pattern(pattern):
"""
Decorator for connecting command handlers to command patterns.
If you use named groups in the pattern, the decorated method will get the
groups as keyword arguments. If the group is optional, remember to give the
argument a default value.
For example, if the command is ``do that thing`` the ``what`` argument will
be ``this thing``::
@handle_pattern('^do (?P<what>.+)$')
def do(what):
...
:param pattern: regexp pattern for matching commands
:type pattern: string
"""
def decorator(func):
match = re.search('([a-z_]+)', pattern)
if match is not None:
mpd_commands.add(match.group())
if pattern in request_handlers:
raise ValueError(u'Tried to redefine handler for %s with %s' % (
pattern, func))
request_handlers[pattern] = func
func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % (
pattern, func.__doc__ or '')
return func
return decorator

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
def disableoutput(frontend, outputid):

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import handle_pattern, MpdUnknownCommand
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdUnknownCommand
@handle_pattern(r'^command_list_begin$')
def command_list_begin(frontend):

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^close$')
def close(frontend):
@ -9,8 +10,7 @@ def close(frontend):
Closes the connection to MPD.
"""
# TODO Does not work after multiprocessing branch merge
#frontend.session.do_close()
pass # TODO
@handle_pattern(r'^kill$')
def kill(frontend):
@ -21,8 +21,7 @@ def kill(frontend):
Kills MPD.
"""
# TODO Does not work after multiprocessing branch merge
#frontend.session.do_kill()
pass # TODO
@handle_pattern(r'^password "(?P<password>[^"]+)"$')
def password_(frontend, password):

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')

View File

@ -0,0 +1,6 @@
from mopidy.frontends.mpd.protocol import handle_pattern
@handle_pattern(r'^$')
def empty(frontend):
"""The original MPD server returns ``OK`` on an empty request."""
pass

View File

@ -1,7 +1,7 @@
import re
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
from mopidy.frontends.mpd.protocol import stored_playlists
from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
def _build_query(mpd_query):
"""

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError,
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
MpdNotImplemented)
@handle_pattern(r'^consume (?P<state>[01])$')

View File

@ -1,5 +1,5 @@
from mopidy.frontends.mpd import (handle_pattern, mpd_commands,
MpdNotImplemented)
from mopidy.frontends.mpd.protocol import handle_pattern, mpd_commands
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^commands$')
def commands(frontend):

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^clearerror$')
def clearerror(frontend):

View File

@ -1,4 +1,5 @@
from mopidy.frontends.mpd import handle_pattern, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_pattern(r'^sticker delete "(?P<field>[^"]+)" '
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')

View File

@ -1,7 +1,7 @@
import datetime as dt
from mopidy.frontends.mpd import (handle_pattern, MpdNoExistError,
MpdNotImplemented)
from mopidy.frontends.mpd.protocol import handle_pattern
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
def listplaylist(frontend, name):

View File

@ -1,21 +1,18 @@
import asynchat
import asyncore
import logging
import multiprocessing
import re
import socket
import sys
from mopidy import get_mpd_protocol_version, settings
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR
from mopidy.process import pickle_connection
from mopidy.utils import indent
from mopidy import settings
from .session import MpdSession
logger = logging.getLogger('mopidy.frontends.mpd.server')
class MpdServer(asyncore.dispatcher):
"""
The MPD server. Creates a :class:`MpdSession` for each client connection.
The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
for each client connection.
"""
def __init__(self, core_queue):
@ -58,65 +55,3 @@ class MpdServer(asyncore.dispatcher):
and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
hostname = '::ffff:%s' % hostname
return hostname
class MpdSession(asynchat.async_chat):
"""
The MPD client session. Keeps track of a single client and dispatches its
MPD requests to the frontend.
"""
def __init__(self, server, client_socket, client_socket_address,
core_queue):
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.set_terminator(LINE_TERMINATOR.encode(ENCODING))
def start(self):
"""Start a new client session."""
self.send_response(u'OK MPD %s' % get_mpd_protocol_version())
def collect_incoming_data(self, data):
"""Collect incoming data into buffer until a terminator is found."""
self.input_buffer.append(data)
def found_terminator(self):
"""Handle request when a terminator is found."""
data = ''.join(self.input_buffer).strip()
self.input_buffer = []
try:
request = data.decode(ENCODING)
logger.debug(u'Input from [%s]:%s: %s', self.client_address,
self.client_port, indent(request))
self.handle_request(request)
except UnicodeDecodeError as e:
logger.warning(u'Received invalid data: %s', e)
def handle_request(self, request):
"""Handle request by sending it to the MPD frontend."""
my_end, other_end = multiprocessing.Pipe()
self.core_queue.put({
'command': 'mpd_request',
'request': request,
'reply_to': pickle_connection(other_end),
})
my_end.poll(None)
response = my_end.recv()
if response is not None:
self.handle_response(response)
def handle_response(self, response):
"""Handle response from the MPD frontend."""
self.send_response(LINE_TERMINATOR.join(response))
def send_response(self, output):
"""Send a response to the client."""
logger.debug(u'Output to [%s]:%s: %s', self.client_address,
self.client_port, indent(output))
output = u'%s%s' % (output, LINE_TERMINATOR)
data = output.encode(ENCODING)
self.push(data)

View File

@ -0,0 +1,70 @@
import asynchat
import logging
import multiprocessing
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.
"""
def __init__(self, server, client_socket, client_socket_address,
core_queue):
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.set_terminator(LINE_TERMINATOR.encode(ENCODING))
def start(self):
"""Start a new client session."""
self.send_response(u'OK MPD %s' % VERSION)
def collect_incoming_data(self, data):
"""Collect incoming data into buffer until a terminator is found."""
self.input_buffer.append(data)
def found_terminator(self):
"""Handle request when a terminator is found."""
data = ''.join(self.input_buffer).strip()
self.input_buffer = []
try:
request = data.decode(ENCODING)
logger.debug(u'Input from [%s]:%s: %s', self.client_address,
self.client_port, indent(request))
self.handle_request(request)
except UnicodeDecodeError as e:
logger.warning(u'Received invalid data: %s', e)
def handle_request(self, request):
"""Handle request by sending it to the MPD frontend."""
my_end, other_end = multiprocessing.Pipe()
self.core_queue.put({
'command': 'mpd_request',
'request': request,
'reply_to': pickle_connection(other_end),
})
my_end.poll(None)
response = my_end.recv()
if response is not None:
self.handle_response(response)
def handle_response(self, response):
"""Handle response from the MPD frontend."""
self.send_response(LINE_TERMINATOR.join(response))
def send_response(self, output):
"""Send a response to the client."""
logger.debug(u'Output to [%s]:%s: %s', self.client_address,
self.client_port, indent(output))
output = u'%s%s' % (output, LINE_TERMINATOR)
data = output.encode(ENCODING)
self.push(data)

View File

@ -1,7 +1,7 @@
import multiprocessing
from mopidy.mixers import BaseMixer
from mopidy.process import pickle_connection
from mopidy.utils.process import pickle_connection
class GStreamerSoftwareMixer(BaseMixer):
"""Mixer which uses GStreamer to control volume in software."""

View File

@ -3,9 +3,9 @@ from serial import Serial
from multiprocessing import Pipe
from mopidy.mixers import BaseMixer
from mopidy.process import BaseProcess
from mopidy.settings import (MIXER_EXT_PORT, MIXER_EXT_SOURCE,
MIXER_EXT_SPEAKERS_A, MIXER_EXT_SPEAKERS_B)
from mopidy.utils.process import BaseProcess
logger = logging.getLogger('mopidy.mixers.nad')
@ -74,7 +74,7 @@ class NadTalker(BaseProcess):
_nad_volume = None
def __init__(self, pipe=None):
super(NadTalker, self).__init__()
super(NadTalker, self).__init__(name='NadTalker')
self.pipe = pipe
self._device = None

View File

@ -9,7 +9,7 @@ import logging
import threading
from mopidy import settings
from mopidy.process import BaseProcess, unpickle_connection
from mopidy.utils.process import BaseProcess, unpickle_connection
logger = logging.getLogger('mopidy.outputs.gstreamer')
@ -44,7 +44,7 @@ class GStreamerProcess(BaseProcess):
"""
def __init__(self, core_queue, output_queue):
super(GStreamerProcess, self).__init__()
super(GStreamerProcess, self).__init__(name='GStreamerProcess')
self.core_queue = core_queue
self.output_queue = output_queue
self.gst_pipeline = None

View File

@ -1,80 +0,0 @@
import logging
import multiprocessing
from multiprocessing.reduction import reduce_connection
import pickle
import sys
from mopidy import SettingsError
logger = logging.getLogger('mopidy.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 run(self):
try:
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'Interrupted by user')
sys.exit(0)
except SettingsError as e:
logger.error(e.message)
sys.exit(1)
except ImportError as e:
logger.error(e)
sys.exit(1)
except Exception as e:
logger.exception(e)
raise e
def run_inside_try(self):
raise NotImplementedError
class CoreProcess(BaseProcess):
def __init__(self, core_queue, output_class, backend_class,
frontend_class):
super(CoreProcess, self).__init__()
self.core_queue = core_queue
self.output_queue = None
self.output_class = output_class
self.backend_class = backend_class
self.frontend_class = frontend_class
self.output = None
self.backend = None
self.frontend = None
def run_inside_try(self):
self.setup()
while True:
message = self.core_queue.get()
self.process_message(message)
def setup(self):
self.output_queue = multiprocessing.Queue()
self.output = self.output_class(self.core_queue, self.output_queue)
self.backend = self.backend_class(self.core_queue, self.output_queue)
self.frontend = self.frontend_class(self.backend)
def process_message(self, message):
if message.get('to') == 'output':
self.output_queue.put(message)
elif message['command'] == 'mpd_request':
response = self.frontend.handle_request(message['request'])
connection = unpickle_connection(message['reply_to'])
connection.send(response)
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)

View File

@ -41,12 +41,15 @@ DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT
#: DUMP_LOG_FILENAME = u'dump.log'
DUMP_LOG_FILENAME = u'dump.log'
#: Protocol frontend to use.
#: List of server frontends to use.
#:
#: Default::
#:
#: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend'
#: FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',)
#:
#: .. note::
#: Currently only the first frontend in the list is used.
FRONTENDS = (u'mopidy.frontends.mpd.MpdFrontend',)
#: Path to folder with local music.
#:
@ -127,13 +130,6 @@ MIXER_MAX_VOLUME = 100
#: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
#: Server to use.
#:
#: Default::
#:
#: SERVER = u'mopidy.frontends.mpd.server.MpdServer'
SERVER = u'mopidy.frontends.mpd.server.MpdServer'
#: Which address Mopidy's MPD server should bind to.
#:
#:Examples:

View File

@ -27,12 +27,3 @@ def get_class(name):
except (ImportError, AttributeError):
raise ImportError("Couldn't load: %s" % name)
return class_object
def indent(string, places=4, linebreak='\n'):
lines = string.split(linebreak)
if len(lines) == 1:
return string
result = u''
for line in lines:
result += linebreak + ' ' * places + line
return result

36
mopidy/utils/log.py Normal file
View File

@ -0,0 +1,36 @@
import logging
import logging.handlers
from mopidy import settings
def setup_logging(verbosity_level, dump):
setup_console_logging(verbosity_level)
if dump:
setup_dump_logging()
def setup_console_logging(verbosity_level):
if verbosity_level == 0:
level = logging.WARNING
elif verbosity_level == 2:
level = logging.DEBUG
else:
level = logging.INFO
logging.basicConfig(format=settings.CONSOLE_LOG_FORMAT, level=level)
def setup_dump_logging():
root = logging.getLogger('')
root.setLevel(logging.DEBUG)
formatter = logging.Formatter(settings.DUMP_LOG_FORMAT)
handler = logging.handlers.RotatingFileHandler(
settings.DUMP_LOG_FILENAME, maxBytes=102400, backupCount=3)
handler.setFormatter(formatter)
root.addHandler(handler)
def indent(string, places=4, linebreak='\n'):
lines = string.split(linebreak)
if len(lines) == 1:
return string
result = u''
for line in lines:
result += linebreak + ' ' * places + line
return result

39
mopidy/utils/process.py Normal file
View File

@ -0,0 +1,39 @@
import logging
import multiprocessing
from multiprocessing.reduction import reduce_connection
import pickle
import sys
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 run(self):
logger.debug(u'%s: Starting process', self.name)
try:
self.run_inside_try()
except KeyboardInterrupt:
logger.info(u'%s: Interrupted by user', self.name)
sys.exit(0)
except SettingsError as e:
logger.error(e.message)
sys.exit(1)
except ImportError as e:
logger.error(e)
sys.exit(1)
except Exception as e:
logger.exception(e)
raise e
def run_inside_try(self):
raise NotImplementedError

View File

@ -6,7 +6,7 @@ import os
import sys
from mopidy import SettingsError
from mopidy.utils import indent
from mopidy.utils.log import indent
logger = logging.getLogger('mopidy.utils.settings')
@ -37,7 +37,7 @@ class SettingsProxy(object):
def current(self):
current = copy(self.default)
current.update(self.local)
return current
return current
def __getattr__(self, attr):
if not self._is_setting(attr):
@ -81,6 +81,8 @@ def validate_settings(defaults, settings):
errors = {}
changed = {
'FRONTEND': 'FRONTENDS',
'SERVER': None,
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
'SERVER_PORT': 'MPD_SERVER_PORT',
'SPOTIFY_LIB_APPKEY': None,

View File

@ -1,13 +1,13 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class AudioOutputHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_enableoutput(self):
result = self.h.handle_request(u'enableoutput "0"')

View File

@ -1,13 +1,13 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class CommandListsTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_command_list_begin(self):
result = self.h.handle_request(u'command_list_begin')

View File

@ -1,13 +1,13 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class ConnectionHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_close(self):
result = self.h.handle_request(u'close')

View File

@ -1,14 +1,14 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class CurrentPlaylistHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_add(self):
needle = Track(uri='dummy://foo')

View File

@ -1,19 +1,21 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend, MpdAckError
from mopidy.frontends.mpd import dispatcher
from mopidy.frontends.mpd.exceptions import MpdAckError
from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern
from mopidy.mixers.dummy import DummyMixer
class RequestHandlerTest(unittest.TestCase):
class MpdDispatcherTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_register_same_pattern_twice_fails(self):
func = lambda: None
try:
frontend.handle_pattern('a pattern')(func)
frontend.handle_pattern('a pattern')(func)
handle_pattern('a pattern')(func)
handle_pattern('a pattern')(func)
self.fail('Registering a pattern twice shoulde raise ValueError')
except ValueError:
pass
@ -28,7 +30,7 @@ class RequestHandlerTest(unittest.TestCase):
def test_finding_handler_for_known_command_returns_handler_and_kwargs(self):
expected_handler = lambda x: None
frontend.request_handlers['known_command (?P<arg1>.+)'] = \
request_handlers['known_command (?P<arg1>.+)'] = \
expected_handler
(handler, kwargs) = self.h.find_handler('known_command an_arg')
self.assertEqual(handler, expected_handler)
@ -41,7 +43,7 @@ class RequestHandlerTest(unittest.TestCase):
def test_handling_known_request(self):
expected = 'magic'
frontend.request_handlers['known request'] = lambda x: expected
request_handlers['known request'] = lambda x: expected
result = self.h.handle_request('known request')
self.assert_(u'OK' in result)
self.assert_(expected in result)

View File

@ -1,6 +1,6 @@
import unittest
from mopidy.frontends.mpd import (MpdAckError, MpdUnknownCommand,
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand,
MpdNotImplemented)
class MpdExceptionsTest(unittest.TestCase):

View File

@ -1,13 +1,13 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class MusicDatabaseHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_count(self):
result = self.h.handle_request(u'count "tag" "needle"')

View File

@ -1,14 +1,14 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class PlaybackOptionsHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_consume_off(self):
result = self.h.handle_request(u'consume "0"')
@ -167,7 +167,7 @@ class PlaybackOptionsHandlerTest(unittest.TestCase):
class PlaybackControlHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_next(self):
result = self.h.handle_request(u'next')

View File

@ -1,13 +1,13 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class ReflectionHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_commands_returns_list_of_all_commands(self):
result = self.h.handle_request(u'commands')

View File

@ -1,14 +1,14 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track
class StatusHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_clearerror(self):
result = self.h.handle_request(u'clearerror')
@ -51,7 +51,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_stats_method(self):
result = frontend.status.stats(self.h)
result = dispatcher.status.stats(self.h)
self.assert_('artists' in result)
self.assert_(int(result['artists']) >= 0)
self.assert_('albums' in result)
@ -72,106 +72,106 @@ class StatusHandlerTest(unittest.TestCase):
self.assert_(u'OK' in result)
def test_status_method_contains_volume_which_defaults_to_0(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('volume' in result)
self.assertEqual(int(result['volume']), 0)
def test_status_method_contains_volume(self):
self.b.mixer.volume = 17
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('volume' in result)
self.assertEqual(int(result['volume']), 17)
def test_status_method_contains_repeat_is_0(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('repeat' in result)
self.assertEqual(int(result['repeat']), 0)
def test_status_method_contains_repeat_is_1(self):
self.b.playback.repeat = 1
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('repeat' in result)
self.assertEqual(int(result['repeat']), 1)
def test_status_method_contains_random_is_0(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('random' in result)
self.assertEqual(int(result['random']), 0)
def test_status_method_contains_random_is_1(self):
self.b.playback.random = 1
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('random' in result)
self.assertEqual(int(result['random']), 1)
def test_status_method_contains_single(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('single' in result)
self.assert_(int(result['single']) in (0, 1))
def test_status_method_contains_consume_is_0(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('consume' in result)
self.assertEqual(int(result['consume']), 0)
def test_status_method_contains_consume_is_1(self):
self.b.playback.consume = 1
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('consume' in result)
self.assertEqual(int(result['consume']), 1)
def test_status_method_contains_playlist(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('playlist' in result)
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
def test_status_method_contains_playlistlength(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('playlistlength' in result)
self.assert_(int(result['playlistlength']) >= 0)
def test_status_method_contains_xfade(self):
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('xfade' in result)
self.assert_(int(result['xfade']) >= 0)
def test_status_method_contains_state_is_play(self):
self.b.playback.state = self.b.playback.PLAYING
result = dict(frontend.status.status(self.h))
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
result = dict(frontend.status.status(self.h))
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
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('state' in result)
self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self):
self.b.current_playlist.append([Track()])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('song' in result)
self.assert_(int(result['song']) >= 0)
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
self.b.current_playlist.append([Track()])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('songid' in result)
self.assertEqual(int(result['songid']), 1)
def test_status_method_when_playing_contains_time_with_no_length(self):
self.b.current_playlist.append([Track(length=None)])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('time' in result)
(position, total) = result['time'].split(':')
position = int(position)
@ -181,7 +181,7 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_playing_contains_time_with_length(self):
self.b.current_playlist.append([Track(length=10000)])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('time' in result)
(position, total) = result['time'].split(':')
position = int(position)
@ -191,13 +191,13 @@ class StatusHandlerTest(unittest.TestCase):
def test_status_method_when_playing_contains_elapsed(self):
self.b.playback.state = self.b.playback.PAUSED
self.b.playback._play_time_accumulated = 59123
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('elapsed' in result)
self.assertEqual(int(result['elapsed']), 59123)
def test_status_method_when_playing_contains_bitrate(self):
self.b.current_playlist.append([Track(bitrate=320)])
self.b.playback.play()
result = dict(frontend.status.status(self.h))
result = dict(dispatcher.status.status(self.h))
self.assert_('bitrate' in result)
self.assertEqual(int(result['bitrate']), 320)

View File

@ -1,13 +1,13 @@
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
class StickersHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_sticker_get(self):
result = self.h.handle_request(

View File

@ -2,14 +2,14 @@ import datetime as dt
import unittest
from mopidy.backends.dummy import DummyBackend
from mopidy.frontends.mpd import frontend
from mopidy.frontends.mpd import dispatcher
from mopidy.mixers.dummy import DummyMixer
from mopidy.models import Track, Playlist
class StoredPlaylistsHandlerTest(unittest.TestCase):
def setUp(self):
self.b = DummyBackend(mixer_class=DummyMixer)
self.h = frontend.MpdFrontend(backend=self.b)
self.h = dispatcher.MpdDispatcher(backend=self.b)
def test_listplaylist(self):
self.b.stored_playlists.playlists = [

View File

@ -3,8 +3,8 @@ import unittest
from mopidy import settings
from mopidy.outputs.gstreamer import GStreamerOutput
from mopidy.process import pickle_connection
from mopidy.utils.path import path_to_uri
from mopidy.utils.process import pickle_connection
from tests import data_folder, SkipTest