Merge branch 'core-as-main-process' into develop
This commit is contained in:
commit
301344d358
@ -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,11 +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:
|
||||
|
||||
|
||||
@ -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()
|
||||
output_class = get_class(settings.OUTPUT)
|
||||
backend_class = get_class(settings.BACKENDS[0])
|
||||
frontend = get_class(settings.FRONTENDS[0])()
|
||||
frontend.start_server(core_queue)
|
||||
core = CoreProcess(core_queue, output_class, backend_class, frontend)
|
||||
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()
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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
88
mopidy/core.py
Normal 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)
|
||||
@ -1,5 +1,5 @@
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.frontends.mpd.server import MpdServer
|
||||
from mopidy.frontends.mpd.process import MpdProcess
|
||||
|
||||
class MpdFrontend(object):
|
||||
"""
|
||||
@ -17,8 +17,8 @@ class MpdFrontend(object):
|
||||
:param core_queue: the core queue
|
||||
:type core_queue: :class:`multiprocessing.Queue`
|
||||
"""
|
||||
self.server = MpdServer(core_queue)
|
||||
self.server.start()
|
||||
self.process = MpdProcess(core_queue)
|
||||
self.process.start()
|
||||
|
||||
def create_dispatcher(self, backend):
|
||||
"""
|
||||
@ -28,6 +28,5 @@ class MpdFrontend(object):
|
||||
:type backend: :class:`mopidy.backends.base.BaseBackend`
|
||||
:rtype: :class:`mopidy.frontends.mpd.dispatcher.MpdDispatcher`
|
||||
"""
|
||||
|
||||
self.dispatcher = MpdDispatcher(backend)
|
||||
return self.dispatcher
|
||||
|
||||
18
mopidy/frontends/mpd/process.py
Normal file
18
mopidy/frontends/mpd/process.py
Normal 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()
|
||||
@ -3,8 +3,8 @@ import logging
|
||||
import multiprocessing
|
||||
|
||||
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
|
||||
from mopidy.process import pickle_connection
|
||||
from mopidy.utils import indent
|
||||
from mopidy.utils.process import pickle_connection
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.mpd.session')
|
||||
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import gst
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from mopidy.process import BaseProcess, unpickle_connection
|
||||
from mopidy.utils.process import BaseProcess, unpickle_connection
|
||||
|
||||
logger = logging.getLogger('mopidy.outputs.gstreamer')
|
||||
|
||||
@ -49,7 +49,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
|
||||
|
||||
@ -1,79 +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):
|
||||
super(CoreProcess, self).__init__()
|
||||
self.core_queue = core_queue
|
||||
self.output_queue = None
|
||||
self.output_class = output_class
|
||||
self.backend_class = backend_class
|
||||
self.output = None
|
||||
self.backend = None
|
||||
self.frontend = frontend
|
||||
self.dispatcher = 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.dispatcher = self.frontend.create_dispatcher(self.backend)
|
||||
|
||||
def process_message(self, message):
|
||||
if message.get('to') == 'output':
|
||||
self.output_queue.put(message)
|
||||
elif message['command'] == 'mpd_request':
|
||||
response = self.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)
|
||||
28
mopidy/utils/log.py
Normal file
28
mopidy/utils/log.py
Normal file
@ -0,0 +1,28 @@
|
||||
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)
|
||||
|
||||
39
mopidy/utils/process.py
Normal file
39
mopidy/utils/process.py
Normal 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
|
||||
@ -2,8 +2,8 @@ import multiprocessing
|
||||
import unittest
|
||||
|
||||
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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user