Merge branch 'develop' into feature/mpd-tokenized-requests
Conflicts: docs/changelog.rst
This commit is contained in:
commit
79ce2ab902
1
AUTHORS
1
AUTHORS
@ -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>
|
||||
|
||||
@ -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>`_
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '0.18.1'
|
||||
__version__ = '0.18.3'
|
||||
|
||||
@ -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__':
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -4,6 +4,3 @@ hostname = 127.0.0.1
|
||||
port = 6680
|
||||
static_dir =
|
||||
zeroconf = Mopidy HTTP server on $hostname
|
||||
|
||||
[loglevels]
|
||||
cherrypy = warning
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user