Merge branch 'develop' into feature/mpd-tokenized-requests

Conflicts:
	docs/changelog.rst
This commit is contained in:
Thomas Adamcik 2014-02-16 23:22:07 +01:00
commit 79ce2ab902
16 changed files with 198 additions and 77 deletions

View File

@ -37,3 +37,4 @@
- Simon de Bakker <simon@simbits.nl>
- Arnaud Barisain-Monrose <abarisain@gmail.com>
- nathanharper <nathan.sam.harper@gmail.com>
- Pierpaolo Frasa <pfrasa@smail.uni-koeln.de>

View File

@ -19,7 +19,7 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
- `Source code <https://github.com/mopidy/mopidy>`_
- `Issue tracker <https://github.com/mopidy/mopidy/issues>`_
- `CI server <https://travis-ci.org/mopidy/mopidy>`_
- `Download development snapshot <https://github.com/mopidy/mopidy/tarball/develop#egg=mopidy-dev>`_
- `Download development snapshot <https://github.com/mopidy/mopidy/archive/develop.tar.gz#egg=mopidy-dev>`_
- IRC: ``#mopidy`` at `irc.freenode.net <http://freenode.net/>`_
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_

View File

@ -5,21 +5,9 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v0.19.0 (unreleased)
v0.19.0 (UNRELEASED)
====================
**Models**
- The type of :attr:`mopidy.models.Playlist.last_modified` has been redefined
from a :class:`datetime.datetime` instance to the number of milliseconds
since Unix epoch as an integer. This makes serialization of the time stamp
simpler.
**MPD**
- Minor refactor of context such that it stores password instead of config.
(Fixes: :issue:`646`)
- Proper command tokenization for MPD requests. This replaces the old regex
based system with an MPD protocol specific tokenizer responsible for breaking
requests into pieces before the handlers have at them.
@ -35,9 +23,49 @@ v0.19.0 (unreleased)
- Adds placeholders for missing MPD commands, preparing the way for bumping the
protocol version once they have been added.
**Windows**
- Network and signal handling has been updated to play nice on windows systems.
v0.18.3 (2014-02-16)
====================
Bug fix release.
- Fix documentation build.
v0.18.2 (2014-02-16)
====================
Bug fix release.
- We now log warnings for wrongly configured extensions, and clearly label them
in ``mopidy config``, but does no longer stop Mopidy from starting because of
misconfigured extensions. (Fixes: :issue:`682`)
- Fix a crash in the server side WebSocket handler caused by connection
problems with clients. (Fixes: :issue:`428`, :issue:`571`)
- Fix the ``time_position`` field of the ``track_playback_ended`` event, which
has been always 0 since v0.18.0. This made scrobbles by Mopidy-Scrobbler not
be persisted by Last.fm, because Mopidy reported that you listened to 0
seconds of each track. (Fixes: :issue:`674`)
- Fix the log setup so that it is possible to increase the amount of logging
from a specific logger using the ``loglevels`` config section. (Fixes:
:issue:`684`)
- Serialization of :class:`~mopidy.models.Playlist` models with the
``last_modified`` field set to a :class:`datetime.datetime` instance did not
work. The type of :attr:`mopidy.models.Playlist.last_modified` has been
redefined from a :class:`datetime.datetime` instance to the number of
milliseconds since Unix epoch as an integer. This makes serialization of the
time stamp simpler.
**Windows**
=======
- Minor refactor of the MPD server context so that Mopidy's MPD protocol
implementation can easier be reused. (Fixes: :issue:`646`)
- Network and signal handling has been updated to play nice on Windows systems.
v0.18.1 (2014-01-23)

View File

@ -51,6 +51,15 @@ Provides a backend for playing music from `Google Play Music
<https://play.google.com/music/>`_.
Mopidy-InternetArchive
======================
https://github.com/tkem/mopidy-internetarchive
Extension for playing music and audio from the `Internet Archive
<https://archive.org/>`_.
Mopidy-MPRIS
============
@ -76,6 +85,14 @@ https://github.com/sauberfred/mopidy-notifier
Extension for displaying track info as User Notifications in Mac OS X.
Mopidy-Podcast
==============
https://github.com/tkem/mopidy-podcast
Extension for browsing RSS feeds of podcasts and stream the episodes.
Mopidy-radio-de
===============
@ -137,3 +154,12 @@ https://github.com/sibuser/mopidy-vkontakte
Provides a backend for playing music from the `VKontakte social network
<http://vk.com/>`_.
Mopidy-Yamaha
=============
https://github.com/knutz3n/mopidy-yamaha
Extension for controlling volume using an external Yamaha network connected
amplifier.

View File

@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.18.1'
__version__ = '0.18.3'

View File

@ -74,20 +74,28 @@ def main():
log.setup_logging(config, verbosity_level, args.save_debug_log)
enabled_extensions = []
extensions = {
'validate': [], 'config': [], 'disabled': [], 'enabled': []}
for extension in installed_extensions:
if not ext.validate_extension(extension):
config[extension.ext_name] = {'enabled': False}
config_errors[extension.ext_name] = {
'enabled': 'extension disabled by self check.'}
extensions['validate'].append(extension)
elif not config[extension.ext_name]['enabled']:
config[extension.ext_name] = {'enabled': False}
config_errors[extension.ext_name] = {
'enabled': 'extension disabled by user config.'}
extensions['disabled'].append(extension)
elif config_errors.get(extension.ext_name):
config[extension.ext_name]['enabled'] = False
config_errors[extension.ext_name]['enabled'] = (
'extension disabled due to config errors.')
extensions['config'].append(extension)
else:
enabled_extensions.append(extension)
extensions['enabled'].append(extension)
log_extension_info(installed_extensions, enabled_extensions)
log_extension_info(installed_extensions, extensions['enabled'])
# Config and deps commands are simply special cased for now.
if args.command == config_cmd:
@ -96,22 +104,22 @@ def main():
elif args.command == deps_cmd:
return args.command.run()
# Remove errors for extensions that are not enabled:
for extension in installed_extensions:
if extension not in enabled_extensions:
config_errors.pop(extension.ext_name, None)
check_config_errors(config_errors)
check_config_errors(config, config_errors, extensions)
if not extensions['enabled']:
logger.error('No extension enabled, exiting...')
sys.exit(1)
# Read-only config from here on, please.
proxied_config = config_lib.Proxy(config)
if args.extension and args.extension not in enabled_extensions:
if args.extension and args.extension not in extensions['enabled']:
logger.error(
'Unable to run command provided by disabled extension %s',
args.extension.ext_name)
return 1
for extension in enabled_extensions:
for extension in extensions['enabled']:
extension.setup(registry)
# Anything that wants to exit after this point must use
@ -173,13 +181,39 @@ def log_extension_info(all_extensions, enabled_extensions):
'Disabled extensions: %s', ', '.join(disabled_names) or 'none')
def check_config_errors(errors):
if not errors:
return
for section in errors:
for key, msg in errors[section].items():
logger.error('Config value %s/%s %s', section, key, msg)
sys.exit(1)
def check_config_errors(config, errors, extensions):
fatal_errors = []
extension_names = {}
all_extension_names = set()
for state in extensions:
extension_names[state] = set(e.ext_name for e in extensions[state])
all_extension_names.update(extension_names[state])
for section in sorted(errors):
if not errors[section]:
continue
if section not in all_extension_names:
logger.warning('Found fatal %s configuration errors:', section)
fatal_errors.append(section)
elif section in extension_names['config']:
del errors[section]['enabled']
logger.warning('Found %s configuration errors, the extension '
'has been automatically disabled:', section)
else:
continue
for field, msg in errors[section].items():
logger.warning(' %s/%s %s', section, field, msg)
if extensions['config']:
logger.warning('Please fix the extension configuration errors or '
'disable the extensions to silence these messages.')
if fatal_errors:
logger.error('Please fix fatal configuration errors, exiting...')
sys.exit(1)
if __name__ == '__main__':

View File

@ -19,20 +19,25 @@ else:
EMPTY_STRING = ''
FETCH_ERROR = (
'Fetching passwords from your keyring failed. Any passwords '
'stored in the keyring will not be available.')
def fetch():
if not dbus:
logger.debug('Fetching from keyring failed: dbus not installed.')
logger.debug('%s (dbus not installed)', FETCH_ERROR)
return []
try:
bus = dbus.SessionBus()
except dbus.exceptions.DBusException as e:
logger.debug('Fetching from keyring failed: %s', e)
logger.debug('%s (%s)', FETCH_ERROR, e)
return []
if not bus.name_has_owner('org.freedesktop.secrets'):
logger.debug(
'Fetching from keyring failed: secrets service not running.')
'%s (org.freedesktop.secrets service not running)', FETCH_ERROR)
return []
service = _service(bus)
@ -47,7 +52,7 @@ def fetch():
items, prompt = service.Unlock(locked)
if prompt != '/':
_prompt(bus, prompt).Dismiss()
logger.debug('Fetching from keyring failed: keyring is locked.')
logger.debug('%s (Keyring is locked)', FETCH_ERROR)
return []
result = []
@ -65,19 +70,20 @@ def set(section, key, value):
Indicates if storage failed or succeeded.
"""
if not dbus:
logger.debug('Saving %s/%s to keyring failed: dbus not installed.',
logger.debug('Saving %s/%s to keyring failed. (dbus not installed)',
section, key)
return False
try:
bus = dbus.SessionBus()
except dbus.exceptions.DBusException as e:
logger.debug('Saving %s/%s to keyring failed: %s', section, key, e)
logger.debug('Saving %s/%s to keyring failed. (%s)', section, key, e)
return False
if not bus.name_has_owner('org.freedesktop.secrets'):
logger.debug(
'Saving %s/%s to keyring failed: secrets service not running.',
'Saving %s/%s to keyring failed. '
'(org.freedesktop.secrets service not running)',
section, key)
return False
@ -101,14 +107,14 @@ def set(section, key, value):
item, prompt = collection.CreateItem(properties, secret, True)
except dbus.exceptions.DBusException as e:
# TODO: catch IsLocked errors etc.
logger.debug('Saving %s/%s to keyring failed: %s', section, key, e)
logger.debug('Saving %s/%s to keyring failed. (%s)', section, key, e)
return False
if prompt == '/':
return True
_prompt(bus, prompt).Dismiss()
logger.debug('Saving secret %s/%s failed: Keyring is locked',
logger.debug('Saving secret %s/%s failed. (Keyring is locked)',
section, key)
return False

View File

@ -306,9 +306,10 @@ class PlaybackController(object):
"""
if self.state != PlaybackState.STOPPED:
backend = self._get_backend()
time_position_before_stop = self.time_position
if not backend or backend.playback.stop().get():
self.state = PlaybackState.STOPPED
self._trigger_track_playback_ended()
self._trigger_track_playback_ended(time_position_before_stop)
if clear_current_track:
self.current_tl_track = None
@ -336,13 +337,14 @@ class PlaybackController(object):
'track_playback_started',
tl_track=self.current_tl_track)
def _trigger_track_playback_ended(self):
def _trigger_track_playback_ended(self, time_position_before_stop):
logger.debug('Triggering track playback ended event')
if self.current_tl_track is None:
return
listener.CoreListener.send(
'track_playback_ended',
tl_track=self.current_tl_track, time_position=self.time_position)
tl_track=self.current_tl_track,
time_position=time_position_before_stop)
def _trigger_playback_state_changed(self, old_state, new_state):
logger.debug('Triggering playback state change event')

View File

@ -100,10 +100,11 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.info('Registered HTTP with Zeroconf as "%s"',
self.zeroconf_service.name)
logger.debug(
'Registered HTTP with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.info('Registering HTTP with Zeroconf failed.')
logger.debug('Registering HTTP with Zeroconf failed.')
def on_stop(self):
if self.zeroconf_service:

View File

@ -4,6 +4,3 @@ hostname = 127.0.0.1
port = 6680
static_dir =
zeroconf = Mopidy HTTP server on $hostname
[loglevels]
cherrypy = warning

View File

@ -1,14 +1,14 @@
from __future__ import unicode_literals
import logging
import socket
import cherrypy
from ws4py.websocket import WebSocket
import ws4py.websocket
from mopidy import core, models
from mopidy.utils import jsonrpc
logger = logging.getLogger(__name__)
@ -43,7 +43,28 @@ class WebSocketResource(object):
cherrypy.request.ws_handler.jsonrpc = self.jsonrpc
class WebSocketHandler(WebSocket):
class _WebSocket(ws4py.websocket.WebSocket):
"""Sub-class ws4py WebSocket with better error handling."""
def send(self, *args, **kwargs):
try:
super(_WebSocket, self).send(*args, **kwargs)
return True
except socket.error as e:
logger.warning('Send message failed: %s', e)
# This isn't really correct, but its the only way to break of out
# the loop in run and trick ws4py into cleaning up.
self.client_terminated = self.server_terminated = True
return False
def close(self, *args, **kwargs):
try:
super(_WebSocket, self).close(*args, **kwargs)
except socket.error as e:
logger.warning('Closing WebSocket failed: %s', e)
class WebSocketHandler(_WebSocket):
def opened(self):
remote = cherrypy.request.remote
logger.debug(
@ -66,8 +87,7 @@ class WebSocketHandler(WebSocket):
remote.ip, remote.port, request)
response = self.jsonrpc.handle_json(request)
if response:
self.send(response)
if response and self.send(response):
logger.debug(
'Sent WebSocket message to %s:%d: %r',
remote.ip, remote.port, response)

View File

@ -1,4 +1,4 @@
from __future__ import unicode_literals
from __future__ import print_function, unicode_literals
import logging
import os
@ -37,17 +37,17 @@ class ClearCommand(commands.Command):
def run(self, args, config):
library = _get_library(args, config)
prompt = 'Are you sure you want to clear the library? [y/N] '
prompt = '\nAre you sure you want to clear the library? [y/N] '
if raw_input(prompt).lower() != 'y':
logging.info('Clearing library aborted.')
print('Clearing library aborted.')
return 0
if library.clear():
logging.info('Library succesfully cleared.')
print('Library successfully cleared.')
return 0
logging.warning('Unable to clear library.')
print('Unable to clear library.')
return 1

View File

@ -48,10 +48,11 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
host=self.hostname, port=self.port)
if self.zeroconf_service.publish():
logger.info('Registered MPD with Zeroconf as "%s"',
self.zeroconf_service.name)
logger.debug(
'Registered MPD with Zeroconf as "%s"',
self.zeroconf_service.name)
else:
logger.info('Registering MPD with Zeroconf failed.')
logger.debug('Registering MPD with Zeroconf failed.')
def on_stop(self):
if self.zeroconf_service:

View File

@ -39,7 +39,6 @@ def setup_logging(config, verbosity_level, save_debug_log):
# added. If not, the other handlers will have no effect.
logging.config.fileConfig(config['logging']['config_file'])
setup_log_levels(config)
setup_console_logging(config, verbosity_level)
if save_debug_log:
setup_debug_logging_to_file(config)
@ -47,11 +46,6 @@ def setup_logging(config, verbosity_level, save_debug_log):
_delayed_handler.release()
def setup_log_levels(config):
for name, level in config['loglevels'].items():
logging.getLogger(name).setLevel(level)
LOG_LEVELS = {
-1: dict(root=logging.ERROR, mopidy=logging.WARNING),
0: dict(root=logging.ERROR, mopidy=logging.INFO),
@ -62,10 +56,15 @@ LOG_LEVELS = {
class VerbosityFilter(logging.Filter):
def __init__(self, verbosity_level):
def __init__(self, verbosity_level, loglevels):
self.verbosity_level = verbosity_level
self.loglevels = loglevels
def filter(self, record):
for name, required_log_level in self.loglevels.items():
if record.name == name or record.name.startswith(name + '.'):
return record.levelno >= required_log_level
if record.name.startswith('mopidy'):
required_log_level = LOG_LEVELS[self.verbosity_level]['mopidy']
else:
@ -79,9 +78,13 @@ def setup_console_logging(config, verbosity_level):
if verbosity_level > max(LOG_LEVELS.keys()):
verbosity_level = max(LOG_LEVELS.keys())
verbosity_filter = VerbosityFilter(verbosity_level)
loglevels = config.get('loglevels', {})
has_debug_loglevels = any([
level < logging.INFO for level in loglevels.values()])
if verbosity_level < 1:
verbosity_filter = VerbosityFilter(verbosity_level, loglevels)
if verbosity_level < 1 and not has_debug_loglevels:
log_format = config['logging']['console_format']
else:
log_format = config['logging']['debug_format']

View File

@ -63,7 +63,7 @@ class Zeroconf(object):
"""
if _is_loopback_address(self.host):
logger.info(
logger.debug(
'Zeroconf publish on loopback interface is not supported.')
return False

View File

@ -43,5 +43,7 @@ class VersionTest(unittest.TestCase):
self.assertLess(SV('0.15.0'), SV('0.16.0'))
self.assertLess(SV('0.16.0'), SV('0.17.0'))
self.assertLess(SV('0.17.0'), SV('0.18.0'))
self.assertLess(SV('0.18.0'), SV(__version__))
self.assertLess(SV(__version__), SV('0.18.2'))
self.assertLess(SV('0.18.0'), SV('0.18.1'))
self.assertLess(SV('0.18.1'), SV('0.18.2'))
self.assertLess(SV('0.18.2'), SV(__version__))
self.assertLess(SV(__version__), SV('0.18.4'))