diff --git a/AUTHORS b/AUTHORS index e51a1966..d7841635 100644 --- a/AUTHORS +++ b/AUTHORS @@ -35,3 +35,6 @@ - Luke Giuliani - Colin Montgomerie - Simon de Bakker +- Arnaud Barisain-Monrose +- nathanharper +- Pierpaolo Frasa diff --git a/docs/changelog.rst b/docs/changelog.rst index d7d53eb1..08cd9a77 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,40 @@ Changelog This changelog is used to track all major changes to Mopidy. +v0.18.2 (2014-02-16) +==================== + +Bug fix release. + +- We now log warnings for wrongly configured extensions, and clearly label them + in :option:`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. + +- 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) ==================== diff --git a/docs/ext/external.rst b/docs/ext/external.rst index 0ead8ac2..43f117bf 100644 --- a/docs/ext/external.rst +++ b/docs/ext/external.rst @@ -76,6 +76,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 =============== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 1367a219..95b296b3 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.1' +__version__ = '0.18.2' diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 1ddd76a4..b447e77d 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() @@ -72,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: @@ -94,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 @@ -171,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__': diff --git a/mopidy/config/keyring.py b/mopidy/config/keyring.py index 6800d2c4..4d251f52 100644 --- a/mopidy/config/keyring.py +++ b/mopidy/config/keyring.py @@ -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 diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index b2acb35a..3164e3b3 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -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') diff --git a/mopidy/http/actor.py b/mopidy/http/actor.py index e7b5cb66..c5787fec 100644 --- a/mopidy/http/actor.py +++ b/mopidy/http/actor.py @@ -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: diff --git a/mopidy/http/ext.conf b/mopidy/http/ext.conf index fc239230..d35229bc 100644 --- a/mopidy/http/ext.conf +++ b/mopidy/http/ext.conf @@ -4,6 +4,3 @@ hostname = 127.0.0.1 port = 6680 static_dir = zeroconf = Mopidy HTTP server on $hostname - -[loglevels] -cherrypy = warning diff --git a/mopidy/http/ws.py b/mopidy/http/ws.py index 4d7aa9a2..286e97e6 100644 --- a/mopidy/http/ws.py +++ b/mopidy/http/ws.py @@ -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) diff --git a/mopidy/local/commands.py b/mopidy/local/commands.py index 85939b43..fb8f5368 100644 --- a/mopidy/local/commands.py +++ b/mopidy/local/commands.py @@ -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 diff --git a/mopidy/models.py b/mopidy/models.py index e1a1270f..42313922 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -418,8 +418,9 @@ class Playlist(ImmutableObject): :type name: string :param tracks: playlist's tracks :type tracks: list of :class:`Track` elements - :param last_modified: playlist's modification time in UTC - :type last_modified: :class:`datetime.datetime` + :param last_modified: + playlist's modification time in milliseconds since Unix epoch + :type last_modified: int """ #: The playlist URI. Read-only. @@ -431,9 +432,10 @@ class Playlist(ImmutableObject): #: The playlist's tracks. Read-only. tracks = tuple() - #: The playlist modification time in UTC. Read-only. + #: The playlist modification time in milliseconds since Unix epoch. + #: Read-only. #: - #: :class:`datetime.datetime`, or :class:`None` if unknown. + #: Integer, or :class:`None` if unknown. last_modified = None def __init__(self, *args, **kwargs): diff --git a/mopidy/mpd/actor.py b/mopidy/mpd/actor.py index 20417a4d..684b4968 100644 --- a/mopidy/mpd/actor.py +++ b/mopidy/mpd/actor.py @@ -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: diff --git a/mopidy/mpd/dispatcher.py b/mopidy/mpd/dispatcher.py index 6aeace9d..0a916408 100644 --- a/mopidy/mpd/dispatcher.py +++ b/mopidy/mpd/dispatcher.py @@ -229,8 +229,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 @@ -246,7 +246,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 a6f9ffcb..41ee9e6a 100644 --- a/mopidy/mpd/protocol/connection.py +++ b/mopidy/mpd/protocol/connection.py @@ -39,7 +39,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 MpdPasswordError('incorrect password') diff --git a/mopidy/mpd/protocol/music_db.py b/mopidy/mpd/protocol/music_db.py index 58681557..e821af6b 100644 --- a/mopidy/mpd/protocol/music_db.py +++ b/mopidy/mpd/protocol/music_db.py @@ -62,30 +62,15 @@ SEARCH_QUERY = r""" $ """ -# TODO Would be nice to get ("?)...\1 working for the quotes here -SEARCH_PAIR_WITHOUT_GROUPS = r""" +SEARCH_PAIR_WITH_GROUPS = r""" + ("?) # Optional quote around the field type \b # Only begin matching at word bundaries - "? # Optional quote around the field type - (?: # A non-capturing group for the field type + ( # A capturing group for the field type """ + SEARCH_FIELDS + """ ) - "? # End of optional quote around the field type + \\1 # End of optional quote around the field type \ # A single space - "[^"]+" # Matching a quoted search string -""" -SEARCH_PAIR_WITHOUT_GROUPS_RE = re.compile( - SEARCH_PAIR_WITHOUT_GROUPS, flags=(re.UNICODE | re.VERBOSE)) - -# TODO Would be nice to get ("?)...\1 working for the quotes here -SEARCH_PAIR_WITH_GROUPS = r""" - \b # Only begin matching at word bundaries - "? # Optional quote around the field type - (?P( # A capturing group for the field type -""" + SEARCH_FIELDS + """ - )) - "? # End of optional quote around the field type - \ # A single space - "(?P[^"]+)" # Capturing a quoted search string + "([^"]+)" # Capturing a quoted search string """ SEARCH_PAIR_WITH_GROUPS_RE = re.compile( SEARCH_PAIR_WITH_GROUPS, flags=(re.UNICODE | re.VERBOSE)) @@ -99,18 +84,18 @@ def _query_from_mpd_search_format(mpd_query): :param mpd_query: the MPD search query :type mpd_query: string """ - pairs = SEARCH_PAIR_WITHOUT_GROUPS_RE.findall(mpd_query) + matches = SEARCH_PAIR_WITH_GROUPS_RE.findall(mpd_query) query = {} - for pair in pairs: - m = SEARCH_PAIR_WITH_GROUPS_RE.match(pair) - field = m.groupdict()['field'].lower() + # discard first field, which just captures optional quote + for _, field, what in matches: + field = field.lower() if field == 'title': field = 'track_name' elif field == 'track': field = 'track_no' elif field in ('file', 'filename'): field = 'uri' - what = m.groupdict()['what'] + if not what: raise ValueError if field in query: diff --git a/mopidy/mpd/protocol/stored_playlists.py b/mopidy/mpd/protocol/stored_playlists.py index a852d795..571dde25 100644 --- a/mopidy/mpd/protocol/stored_playlists.py +++ b/mopidy/mpd/protocol/stored_playlists.py @@ -1,6 +1,6 @@ -from __future__ import unicode_literals +from __future__ import division, unicode_literals -import datetime as dt +import datetime from mopidy.mpd.exceptions import MpdNoExistError, MpdNotImplemented from mopidy.mpd.protocol import handle_request @@ -80,16 +80,26 @@ def listplaylists(context): continue name = context.lookup_playlist_name_from_uri(playlist.uri) result.append(('playlist', name)) - last_modified = ( - playlist.last_modified or dt.datetime.utcnow()).isoformat() - # Remove microseconds - last_modified = last_modified.split('.')[0] - # Add time zone information - last_modified = last_modified + 'Z' - result.append(('Last-Modified', last_modified)) + result.append(('Last-Modified', _get_last_modified(playlist))) return result +def _get_last_modified(playlist): + """Formats last modified timestamp of a playlist for MPD. + + Time in UTC with second precision, formatted in the ISO 8601 format, with + the "Z" time zone marker for UTC. For example, "1970-01-01T00:00:00Z". + """ + if playlist.last_modified is None: + # If unknown, assume the playlist is modified + dt = datetime.datetime.utcnow() + else: + dt = datetime.datetime.utcfromtimestamp( + playlist.last_modified / 1000.0) + dt = dt.replace(microsecond=0) + return '%sZ' % dt.isoformat() + + @handle_request( r'load\ "(?P[^"]+)"(\ "(?P\d+):(?P\d+)*")*$') def load(context, name, start=None, end=None): diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 6004b9f9..cde07693 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -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'] 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/mopidy/zeroconf.py b/mopidy/zeroconf.py index e95b1792..1111975f 100644 --- a/mopidy/zeroconf.py +++ b/mopidy/zeroconf.py @@ -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 diff --git a/tests/mpd/protocol/test_music_db.py b/tests/mpd/protocol/test_music_db.py index 8d74fb95..c0dcf83d 100644 --- a/tests/mpd/protocol/test_music_db.py +++ b/tests/mpd/protocol/test_music_db.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import datetime import unittest from mopidy.mpd.protocol import music_db @@ -12,7 +11,7 @@ from tests.mpd import protocol class QueryFromMpdSearchFormatTest(unittest.TestCase): def test_dates_are_extracted(self): result = music_db._query_from_mpd_search_format( - 'Date "1974-01-02" Date "1975"') + 'Date "1974-01-02" "Date" "1975"') self.assertEqual(result['date'][0], '1974-01-02') self.assertEqual(result['date'][1], '1975') @@ -237,7 +236,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqual(response1, response2) def test_lsinfo_without_path_returns_same_as_for_root(self): - last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + last_modified = 1390942873222 self.backend.playlists.playlists = [ Playlist(name='a', uri='dummy:/a', last_modified=last_modified)] @@ -246,7 +245,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqual(response1, response2) def test_lsinfo_with_empty_path_returns_same_as_for_root(self): - last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + last_modified = 1390942873222 self.backend.playlists.playlists = [ Playlist(name='a', uri='dummy:/a', last_modified=last_modified)] @@ -255,14 +254,14 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqual(response1, response2) def test_lsinfo_for_root_includes_playlists(self): - last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + last_modified = 1390942873222 self.backend.playlists.playlists = [ Playlist(name='a', uri='dummy:/a', last_modified=last_modified)] self.sendRequest('lsinfo "/"') self.assertInResponse('playlist: a') - # Date without microseconds and with time zone information - self.assertInResponse('Last-Modified: 2001-03-17T13:41:17Z') + # Date without milliseconds and with time zone information + self.assertInResponse('Last-Modified: 2014-01-28T21:01:13Z') self.assertInResponse('OK') def test_lsinfo_for_root_includes_dirs_for_each_lib_with_content(self): diff --git a/tests/mpd/protocol/test_stored_playlists.py b/tests/mpd/protocol/test_stored_playlists.py index 636c5c2c..857ed03e 100644 --- a/tests/mpd/protocol/test_stored_playlists.py +++ b/tests/mpd/protocol/test_stored_playlists.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -import datetime - from mopidy.models import Track, Playlist from tests.mpd import protocol @@ -78,14 +76,14 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_listplaylists(self): - last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + last_modified = 1390942873222 self.backend.playlists.playlists = [ Playlist(name='a', uri='dummy:a', last_modified=last_modified)] self.sendRequest('listplaylists') self.assertInResponse('playlist: a') - # Date without microseconds and with time zone information - self.assertInResponse('Last-Modified: 2001-03-17T13:41:17Z') + # Date without milliseconds and with time zone information + self.assertInResponse('Last-Modified: 2014-01-28T21:01:13Z') self.assertInResponse('OK') def test_listplaylists_duplicate(self): @@ -99,7 +97,7 @@ class PlaylistsHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') def test_listplaylists_ignores_playlists_without_name(self): - last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + last_modified = 1390942873222 self.backend.playlists.playlists = [ Playlist(name='', uri='dummy:', last_modified=last_modified)] diff --git a/tests/test_models.py b/tests/test_models.py index 9a4f97b7..13ab637f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import datetime import json import unittest @@ -842,7 +841,7 @@ class PlaylistTest(unittest.TestCase): self.assertEqual(playlist.length, 3) def test_last_modified(self): - last_modified = datetime.datetime.utcnow() + last_modified = 1390942873000 playlist = Playlist(last_modified=last_modified) self.assertEqual(playlist.last_modified, last_modified) self.assertRaises( @@ -850,7 +849,7 @@ class PlaylistTest(unittest.TestCase): def test_with_new_uri(self): tracks = [Track()] - last_modified = datetime.datetime.utcnow() + last_modified = 1390942873000 playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) @@ -862,7 +861,7 @@ class PlaylistTest(unittest.TestCase): def test_with_new_name(self): tracks = [Track()] - last_modified = datetime.datetime.utcnow() + last_modified = 1390942873000 playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) @@ -874,7 +873,7 @@ class PlaylistTest(unittest.TestCase): def test_with_new_tracks(self): tracks = [Track()] - last_modified = datetime.datetime.utcnow() + last_modified = 1390942873000 playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) @@ -887,8 +886,8 @@ class PlaylistTest(unittest.TestCase): def test_with_new_last_modified(self): tracks = [Track()] - last_modified = datetime.datetime.utcnow() - new_last_modified = last_modified + datetime.timedelta(1) + last_modified = 1390942873000 + new_last_modified = last_modified + 1000 playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) diff --git a/tests/test_version.py b/tests/test_version.py index 23c93f01..737a21f3 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -43,5 +43,6 @@ 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(__version__)) + self.assertLess(SV(__version__), SV('0.18.3'))