diff --git a/AUTHORS b/AUTHORS index e51a1966..6853d5ab 100644 --- a/AUTHORS +++ b/AUTHORS @@ -35,3 +35,5 @@ - Luke Giuliani - Colin Montgomerie - Simon de Bakker +- Arnaud Barisain-Monrose +- nathanharper diff --git a/docs/changelog.rst b/docs/changelog.rst index 61234f6d..df7de99a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,34 @@ Changelog This changelog is used to track all major changes to Mopidy. -v0.18.0 (UNRELEASED) + +v0.19.0 (unreleased) +==================== + +**MPD** + +- Minor refactor of context such that it stores password instead of config. + (Fixes: :issue:`646`) + +**Windows** + +- Network and signal handling has been updated to play nice on windows systems. + + +v0.18.1 (2014-01-23) +==================== + +Bug fix release. + +- Disable extension instead of crashing if a dependency has the wrong + version. (Fixes: :issue:`657`) + +- Make logging work to both console, debug log file, and any custom logging + setup from :confval:`logging/config_file` at the same time. (Fixes: + :issue:`661`) + + +v0.18.0 (2014-01-19) ==================== The focus of 0.18 have been on two fronts: the local library and browsing. @@ -21,8 +48,8 @@ the local and Spotify backends. It is also used by the new Mopidy-Dirble extension to provide you with a directory of Internet radio stations from all over the world. -Since the release of 0.17, we've closed or merged 47 issues and pull requests -through about 270 commits by :ref:`11 people `, including six new +Since the release of 0.17, we've closed or merged 49 issues and pull requests +through about 285 commits by :ref:`11 people `, including six new guys. Thanks to everyone that has contributed! **Core API** @@ -1831,9 +1858,8 @@ to this problem. - MPD frontend: - Add support for password authentication. See - :attr:`mopidy.settings.MPD_SERVER_PASSWORD` and - :ref:`use-mpd-on-a-network` for details on how to use it. (Fixes: - :issue:`41`) + :attr:`mopidy.settings.MPD_SERVER_PASSWORD` for details on how to use it. + (Fixes: :issue:`41`) - Support ``setvol 50`` without quotes around the argument. Fixes volume control in Droid MPD. diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 623e202e..1367a219 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring) warnings.filterwarnings('ignore', 'could not open display') -__version__ = '0.18.0' +__version__ = '0.18.1' diff --git a/mopidy/__main__.py b/mopidy/__main__.py index ac5e2102..05394bc2 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -37,7 +37,9 @@ def main(): logger.info('Starting Mopidy %s', versioning.get_version()) signal.signal(signal.SIGTERM, process.exit_handler) - signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) + # Windows does not have signal.SIGUSR1 + if hasattr(signal, 'SIGUSR1'): + signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) try: registry = ext.Registry() @@ -70,8 +72,7 @@ def main(): if args.verbosity_level: verbosity_level += args.verbosity_level - log.setup_logging( - config, installed_extensions, verbosity_level, args.save_debug_log) + log.setup_logging(config, verbosity_level, args.save_debug_log) enabled_extensions = [] for extension in installed_extensions: diff --git a/mopidy/ext.py b/mopidy/ext.py index a58090cc..4887f295 100644 --- a/mopidy/ext.py +++ b/mopidy/ext.py @@ -222,6 +222,12 @@ def validate_extension(extension): 'Disabled extension %s: Dependency %s not found', extension.ext_name, ex) return False + except pkg_resources.VersionConflict as ex: + found, required = ex.args + logger.info( + 'Disabled extension %s: %s required, but found %s at %s', + extension.ext_name, required, found, found.location) + return False try: extension.validate_environment() diff --git a/mopidy/local/json.py b/mopidy/local/json.py index ad6be5d3..10611f6f 100644 --- a/mopidy/local/json.py +++ b/mopidy/local/json.py @@ -66,12 +66,15 @@ class _BrowseCache(object): for i in reversed(range(len(parts))): directory = '/'.join(parts[:i+1]) uri = translator.path_to_local_directory_uri(directory) + # First dir we process is our parent if not parent_uri: parent_uri = uri # We found ourselves and we exist, done. if uri in self._cache: + if child: + self._cache[uri][child.uri] = child break # Initialize ourselves, store child if present, and add diff --git a/mopidy/mpd/dispatcher.py b/mopidy/mpd/dispatcher.py index d91c8a66..61a07fb2 100644 --- a/mopidy/mpd/dispatcher.py +++ b/mopidy/mpd/dispatcher.py @@ -218,8 +218,8 @@ class MpdContext(object): #: The current :class:`mopidy.mpd.MpdSession`. session = None - #: The Mopidy configuration. - config = None + #: The MPD password + password = None #: The Mopidy core API. An instance of :class:`mopidy.core.Core`. core = None @@ -235,7 +235,8 @@ class MpdContext(object): def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher self.session = session - self.config = config + if config is not None: + self.password = config['mpd']['password'] self.core = core self.events = set() self.subscriptions = set() diff --git a/mopidy/mpd/protocol/connection.py b/mopidy/mpd/protocol/connection.py index b324add8..41896acf 100644 --- a/mopidy/mpd/protocol/connection.py +++ b/mopidy/mpd/protocol/connection.py @@ -37,7 +37,7 @@ def password(context, password): This is used for authentication with the server. ``PASSWORD`` is simply the plaintext password. """ - if password == context.config['mpd']['password']: + if password == context.password: context.dispatcher.authenticated = True else: raise exceptions.MpdPasswordError('incorrect password') diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 9c88b368..6004b9f9 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -27,23 +27,23 @@ _delayed_handler = DelayedHandler() def bootstrap_delayed_logging(): root = logging.getLogger('') - root.setLevel(logging.DEBUG) + root.setLevel(logging.NOTSET) root.addHandler(_delayed_handler) -def setup_logging(config, extensions, verbosity_level, save_debug_log): - setup_log_levels(config) - - setup_console_logging(config, extensions, verbosity_level) - - if save_debug_log: - setup_debug_logging_to_file(config, extensions) - +def setup_logging(config, verbosity_level, save_debug_log): logging.captureWarnings(True) if config['logging']['config_file']: + # Logging config from file must be read before other handlers are + # 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) + _delayed_handler.release() @@ -61,46 +61,43 @@ LOG_LEVELS = { } -def setup_console_logging(config, extensions, verbosity_level): +class VerbosityFilter(logging.Filter): + def __init__(self, verbosity_level): + self.verbosity_level = verbosity_level + + def filter(self, record): + if record.name.startswith('mopidy'): + required_log_level = LOG_LEVELS[self.verbosity_level]['mopidy'] + else: + required_log_level = LOG_LEVELS[self.verbosity_level]['root'] + return record.levelno >= required_log_level + + +def setup_console_logging(config, verbosity_level): if verbosity_level < min(LOG_LEVELS.keys()): verbosity_level = min(LOG_LEVELS.keys()) if verbosity_level > max(LOG_LEVELS.keys()): verbosity_level = max(LOG_LEVELS.keys()) + verbosity_filter = VerbosityFilter(verbosity_level) + if verbosity_level < 1: log_format = config['logging']['console_format'] else: log_format = config['logging']['debug_format'] formatter = logging.Formatter(log_format) - root_handler = logging.StreamHandler() - root_handler.setFormatter(formatter) - root_handler.setLevel(LOG_LEVELS[verbosity_level]['root']) - logging.getLogger('').addHandler(root_handler) + handler = logging.StreamHandler() + handler.addFilter(verbosity_filter) + handler.setFormatter(formatter) - mopidy_handler = logging.StreamHandler() - mopidy_handler.setFormatter(formatter) - mopidy_handler.setLevel(LOG_LEVELS[verbosity_level]['mopidy']) - add_mopidy_handler(extensions, mopidy_handler) + logging.getLogger('').addHandler(handler) -def setup_debug_logging_to_file(config, extensions): +def setup_debug_logging_to_file(config): formatter = logging.Formatter(config['logging']['debug_format']) handler = logging.handlers.RotatingFileHandler( config['logging']['debug_file'], maxBytes=10485760, backupCount=3) handler.setFormatter(formatter) logging.getLogger('').addHandler(handler) - - # We must add our handler explicitly, since the mopidy* handlers don't - # propagate to the root handler. - add_mopidy_handler(extensions, handler) - - -def add_mopidy_handler(extensions, handler): - names = ['mopidy_%s' % ext.ext_name for ext in extensions] - names.append('mopidy') - for name in names: - logger = logging.getLogger(name) - logger.propagate = False - logger.addHandler(handler) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index bb1edbc4..20587eac 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -5,6 +5,7 @@ import gobject import logging import re import socket +import sys import threading import pykka @@ -43,7 +44,12 @@ def create_socket(): if has_ipv6: sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) # Explicitly configure socket to work for both IPv4 and IPv6 - sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + if hasattr(socket, 'IPPROTO_IPV6'): + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + elif sys.platform == 'win32': # also match 64bit windows. + # Python 2.7 on windows does not have the IPPROTO_IPV6 constant + # Use values extracted from Windows Vista/7/8's header + sock.setsockopt(41, 27, 0) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) diff --git a/tests/local/test_json.py b/tests/local/test_json.py index 9c8686e9..54afefe7 100644 --- a/tests/local/test_json.py +++ b/tests/local/test_json.py @@ -7,20 +7,27 @@ from mopidy.models import Ref class BrowseCacheTest(unittest.TestCase): + maxDiff = None + def setUp(self): - self.uris = [b'local:track:foo/bar/song1', - b'local:track:foo/bar/song2', - b'local:track:foo/song3'] + self.uris = ['local:track:foo/bar/song1', + 'local:track:foo/bar/song2', + 'local:track:foo/baz/song3', + 'local:track:foo/song4', + 'local:track:song5'] self.cache = json._BrowseCache(self.uris) def test_lookup_root(self): - expected = [Ref.directory(uri='local:directory:foo', name='foo')] + expected = [Ref.directory(uri='local:directory:foo', name='foo'), + Ref.track(uri='local:track:song5', name='song5')] self.assertEqual(expected, self.cache.lookup('local:directory')) def test_lookup_foo(self): expected = [Ref.directory(uri='local:directory:foo/bar', name='bar'), - Ref.track(uri=self.uris[2], name='song3')] - self.assertEqual(expected, self.cache.lookup('local:directory:foo')) + Ref.directory(uri='local:directory:foo/baz', name='baz'), + Ref.track(uri=self.uris[3], name='song4')] + result = self.cache.lookup('local:directory:foo') + self.assertEqual(expected, result) def test_lookup_foo_bar(self): expected = [Ref.track(uri=self.uris[0], name='song1'), @@ -29,4 +36,5 @@ class BrowseCacheTest(unittest.TestCase): expected, self.cache.lookup('local:directory:foo/bar')) def test_lookup_foo_baz(self): - self.assertEqual([], self.cache.lookup('local:directory:foo/baz')) + result = self.cache.lookup('local:directory:foo/unknown') + self.assertEqual([], result) diff --git a/tests/test_version.py b/tests/test_version.py index 5fb1a60d..23c93f01 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -42,5 +42,6 @@ class VersionTest(unittest.TestCase): self.assertLess(SV('0.14.2'), SV('0.15.0')) 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(__version__)) - self.assertLess(SV(__version__), SV('0.18.1')) + 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'))