diff --git a/AUTHORS b/AUTHORS index d536c059..45e1a37e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ - Matt Bray - Trygve Aaberge - Wouter van Wijk +- Jeremy B. Merrill diff --git a/docs/_static/woutervanwijk-mopidy-webclient.png b/docs/_static/woutervanwijk-mopidy-webclient.png new file mode 100644 index 00000000..0dd99acc Binary files /dev/null and b/docs/_static/woutervanwijk-mopidy-webclient.png differ diff --git a/docs/api/backends.rst b/docs/api/backends.rst index f0aadd53..32c04d37 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -46,5 +46,6 @@ Backend implementations ======================= * :mod:`mopidy.backends.dummy` -* :mod:`mopidy.backends.spotify` * :mod:`mopidy.backends.local` +* :mod:`mopidy.backends.spotify` +* :mod:`mopidy.backends.stream` diff --git a/docs/changes.rst b/docs/changes.rst index 89707b6a..c2b076fc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,10 +10,39 @@ v0.12.0 (in development) (in development) -**Spotify** +- Make Mopidy work on early Python 2.6 versions. (Fixes: :issue:`302`) + + - ``optparse`` fails if the first argument to ``add_option`` is a unicode + string on Python < 2.6.2rc1. + + - ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python + < 2.6.5rc1. + +**Spotify backend** - Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`) +**Local backend** + +- Make ``mopidy-scan`` support symlinks. + +**Stream backend** + +We've added a new backend for playing audio streams, the :mod:`stream backend +`. It is activated by default. + +The stream backend supports the intersection of what your GStreamer +installation supports and what protocols are included in the +:attr:`mopidy.settings.STREAM_PROTOCOLS` settings. + +Current limitations: + +- No metadata about the current track in the stream is available. + +- Playlists are not parsed, so you can't play e.g. a M3U or PLS file which + contains stream URIs. You need to extract the stream URL from the playlist + yourself. See :issue:`303` for progress on this. + v0.11.0 (2012-12-24) ==================== diff --git a/docs/clients/http.rst b/docs/clients/http.rst index e41adb5b..c6e8b7d8 100644 --- a/docs/clients/http.rst +++ b/docs/clients/http.rst @@ -4,11 +4,26 @@ HTTP clients ************ -Mopidy added an :ref:`http-frontend` in 0.10 which provides the building blocks -needed for creating web clients for Mopidy with the help of a WebSocket and a -JavaScript library provided by Mopidy. +Mopidy added an :ref:`HTTP frontend ` in 0.10 which provides the +building blocks needed for creating web clients for Mopidy with the help of a +WebSocket and a JavaScript library provided by Mopidy. This page will list any HTTP/web Mopidy clients. If you've created one, please notify us so we can include your client on this page. See :ref:`http-frontend` for details on how to build your own web client. + + +woutervanwijk/Mopidy-Webclient +============================== + +.. image:: /_static/woutervanwijk-mopidy-webclient.png + :width: 410 + :height: 511 + +The first web client for Mopidy is still under development, but is already very +usable. It targets both desktop and mobile browsers. + +To try it out, get a copy of https://github.com/woutervanwijk/Mopidy-WebClient +and point the :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` setting towards +your copy of the web client. diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index fbb07364..8a4d9409 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -210,6 +210,10 @@ software packages, as Wheezy is going to be the next release of Debian. aplay /usr/share/sounds/alsa/Front_Center.wav + If you hear a voice saying "Front Center," then your sound is working. Don't + be concerned if this test sound includes static, output from Mopidy will not. + Test your sound with gstreamer to determine sound quality. + To make the change to analog output stick, you can add the ``amixer`` command to e.g. ``/etc/rc.local``, which will be executed when the system is booting. diff --git a/docs/modules/backends/stream.rst b/docs/modules/backends/stream.rst new file mode 100644 index 00000000..73e53048 --- /dev/null +++ b/docs/modules/backends/stream.rst @@ -0,0 +1,7 @@ +*********************************************** +:mod:`mopidy.backends.stream` -- Stream backend +*********************************************** + +.. automodule:: mopidy.backends.stream + :synopsis: Backend for playing audio streams + :members: diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 952f158c..e111fcef 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -79,37 +79,40 @@ def main(): def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) + # NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use + # bytestrings for the first argument to ``add_option`` + # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( - '--help-gst', + b'--help-gst', action='store_true', dest='help_gst', help='show GStreamer help options') parser.add_option( - '-i', '--interactive', + b'-i', '--interactive', action='store_true', dest='interactive', help='ask interactively for required settings which are missing') parser.add_option( - '-q', '--quiet', + b'-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') parser.add_option( - '-v', '--verbose', + b'-v', '--verbose', action='count', default=1, dest='verbosity_level', help='more output (debug level)') parser.add_option( - '--save-debug-log', + b'--save-debug-log', action='store_true', dest='save_debug_log', help='save debug log to "./mopidy.log"') parser.add_option( - '--list-settings', + b'--list-settings', action='callback', callback=settings_utils.list_settings_optparse_callback, help='list current settings') parser.add_option( - '--list-deps', + b'--list-deps', action='callback', callback=deps.list_deps_optparse_callback, help='list dependencies and their versions') parser.add_option( - '--debug-thread', + b'--debug-thread', action='store_true', dest='debug_thread', help='run background thread that dumps tracebacks on SIGUSR1') return parser.parse_args(args=mopidy_args)[0] diff --git a/mopidy/audio/__init__.py b/mopidy/audio/__init__.py index 7cf1dcee..5adb333c 100644 --- a/mopidy/audio/__init__.py +++ b/mopidy/audio/__init__.py @@ -4,3 +4,5 @@ from __future__ import unicode_literals from .actor import Audio from .listener import AudioListener from .constants import PlaybackState +from .utils import (calculate_duration, create_buffer, millisecond_to_clocktime, + supported_uri_schemes) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py new file mode 100644 index 00000000..9d0f46dd --- /dev/null +++ b/mopidy/audio/utils.py @@ -0,0 +1,50 @@ +from __future__ import unicode_literals + +import pygst +pygst.require('0.10') +import gst + + +def calculate_duration(num_samples, sample_rate): + """Determine duration of samples using GStreamer helper for precise math.""" + return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate) + + +def create_buffer(data, capabilites=None, timestamp=None, duration=None): + """Create a new GStreamer buffer based on provided data. + + Mainly intended to keep gst imports out of non-audio modules. + """ + buffer_ = gst.Buffer(data) + if capabilites: + if isinstance(capabilites, basestring): + capabilites = gst.caps_from_string(capabilites) + buffer_.set_caps(capabilites) + if timestamp: + buffer_.timestamp = timestamp + if duration: + buffer_.duration = duration + return buffer_ + + +def millisecond_to_clocktime(value): + """Convert a millisecond time to internal gstreamer time.""" + return value * gst.MSECOND + + +def supported_uri_schemes(uri_schemes): + """Determine which URIs we can actually support from provided whitelist. + + :param uri_schemes: list/set of URIs to check support for. + :type uri_schemes: list or set or URI schemes as strings. + :rtype: set of URI schemes we can support via this GStreamer install. + """ + supported_schemes = set() + registry = gst.registry_get_default() + + for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): + for uri in factory.get_uri_protocols(): + if uri in uri_schemes: + supported_schemes.add(uri) + + return supported_schemes diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 8250a24c..f49aa89b 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -57,9 +57,9 @@ class BaseLibraryProvider(object): """ See :meth:`mopidy.core.LibraryController.find_exact`. - *MUST be implemented by subclass.* + *MAY be implemented by subclass.* """ - raise NotImplementedError + pass def lookup(self, uri): """ @@ -73,17 +73,17 @@ class BaseLibraryProvider(object): """ See :meth:`mopidy.core.LibraryController.refresh`. - *MUST be implemented by subclass.* + *MAY be implemented by subclass.* """ - raise NotImplementedError + pass def search(self, **query): """ See :meth:`mopidy.core.LibraryController.search`. - *MUST be implemented by subclass.* + *MAY be implemented by subclass.* """ - raise NotImplementedError + pass class BasePlaybackProvider(object): diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 390fd92a..157804b4 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -98,6 +98,9 @@ def _convert_mpd_data(data, tracks, music_dir): if not data: return + # NOTE: kwargs are explicitly made bytestrings to work on Python + # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for details. + track_kwargs = {} album_kwargs = {} artist_kwargs = {} @@ -105,38 +108,38 @@ def _convert_mpd_data(data, tracks, music_dir): if 'track' in data: if '/' in data['track']: - album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) - track_kwargs['track_no'] = int(data['track'].split('/')[0]) + album_kwargs[b'num_tracks'] = int(data['track'].split('/')[1]) + track_kwargs[b'track_no'] = int(data['track'].split('/')[0]) else: - track_kwargs['track_no'] = int(data['track']) + track_kwargs[b'track_no'] = int(data['track']) if 'artist' in data: - artist_kwargs['name'] = data['artist'] - albumartist_kwargs['name'] = data['artist'] + artist_kwargs[b'name'] = data['artist'] + albumartist_kwargs[b'name'] = data['artist'] if 'albumartist' in data: - albumartist_kwargs['name'] = data['albumartist'] + albumartist_kwargs[b'name'] = data['albumartist'] if 'album' in data: - album_kwargs['name'] = data['album'] + album_kwargs[b'name'] = data['album'] if 'title' in data: - track_kwargs['name'] = data['title'] + track_kwargs[b'name'] = data['title'] if 'date' in data: - track_kwargs['date'] = data['date'] + track_kwargs[b'date'] = data['date'] if 'musicbrainz_trackid' in data: - track_kwargs['musicbrainz_id'] = data['musicbrainz_trackid'] + track_kwargs[b'musicbrainz_id'] = data['musicbrainz_trackid'] if 'musicbrainz_albumid' in data: - album_kwargs['musicbrainz_id'] = data['musicbrainz_albumid'] + album_kwargs[b'musicbrainz_id'] = data['musicbrainz_albumid'] if 'musicbrainz_artistid' in data: - artist_kwargs['musicbrainz_id'] = data['musicbrainz_artistid'] + artist_kwargs[b'musicbrainz_id'] = data['musicbrainz_artistid'] if 'musicbrainz_albumartistid' in data: - albumartist_kwargs['musicbrainz_id'] = ( + albumartist_kwargs[b'musicbrainz_id'] = ( data['musicbrainz_albumartistid']) if data['file'][0] == '/': @@ -147,18 +150,18 @@ def _convert_mpd_data(data, tracks, music_dir): if artist_kwargs: artist = Artist(**artist_kwargs) - track_kwargs['artists'] = [artist] + track_kwargs[b'artists'] = [artist] if albumartist_kwargs: albumartist = Artist(**albumartist_kwargs) - album_kwargs['artists'] = [albumartist] + album_kwargs[b'artists'] = [albumartist] if album_kwargs: album = Album(**album_kwargs) - track_kwargs['album'] = album + track_kwargs[b'album'] = album - track_kwargs['uri'] = path_to_uri(music_dir, path) - track_kwargs['length'] = int(data.get('time', 0)) * 1000 + track_kwargs[b'uri'] = path_to_uri(music_dir, path) + track_kwargs[b'length'] = int(data.get('time', 0)) * 1000 track = Track(**track_kwargs) tracks.add(track) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index a8a9bcd6..96e5f616 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -163,7 +163,9 @@ class SpotifyLibraryProvider(base.BaseLibraryProvider): translator.to_mopidy_track(t) for t in results.tracks()]) future.set(search_result) - if not self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT): + # Wait always returns None on python 2.6 :/ + self.backend.spotify.connected.wait(settings.SPOTIFY_TIMEOUT) + if not self.backend.spotify.connected.is_set(): logger.debug('Not connected: Spotify search cancelled') return SearchResult(uri='spotify:search') diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index c148972c..bda17634 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -5,6 +5,7 @@ import functools from spotify import Link, SpotifyError +from mopidy import audio from mopidy.backends import base @@ -30,6 +31,10 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): 'width=(int)16, depth=(int)16, signed=(boolean)true, ' 'rate=(int)44100') + def __init__(self, *args, **kwargs): + super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs) + self._first_seek = False + def play(self, track): if track.uri is None: return False @@ -42,10 +47,13 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): seek_data_callback_bound = functools.partial( seek_data_callback, spotify_backend) + self._first_seek = True + try: self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) + self.backend.spotify.buffer_timestamp = 0 self.audio.prepare_change() self.audio.set_appsrc( @@ -75,5 +83,12 @@ class SpotifyPlaybackProvider(base.BasePlaybackProvider): def on_seek_data(self, time_position): logger.debug('playback.on_seek_data(%d) called', time_position) - self.backend.spotify.next_buffer_timestamp = time_position + + if time_position == 0 and self._first_seek: + self._first_seek = False + logger.debug('Skipping seek due to issue #300') + return + + self.backend.spotify.buffer_timestamp = audio.millisecond_to_clocktime( + time_position) self.backend.spotify.session.seek(time_position) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index a9c0884e..6f386aae 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -1,9 +1,5 @@ from __future__ import unicode_literals -import pygst -pygst.require('0.10') -import gst - import logging import os import threading @@ -47,7 +43,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): self.connected = threading.Event() self.push_audio_data = True - self.next_buffer_timestamp = None + self.buffer_timestamp = 0 self.container_manager = None self.playlist_manager = None @@ -85,6 +81,7 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): def logged_out(self, session): """Callback used by pyspotify""" logger.info('Disconnected from Spotify') + self.connected.clear() def metadata_updated(self, session): """Callback used by pyspotify""" @@ -125,11 +122,14 @@ class SpotifySessionManager(process.BaseThread, PyspotifySessionManager): 'sample_rate': sample_rate, 'channels': channels, } - buffer_ = gst.Buffer(bytes(frames)) - buffer_.set_caps(gst.caps_from_string(capabilites)) - if self.next_buffer_timestamp is not None: - buffer_.timestamp = self.next_buffer_timestamp * gst.MSECOND - self.next_buffer_timestamp = None + + duration = audio.calculate_duration(num_frames, sample_rate) + buffer_ = audio.create_buffer(bytes(frames), + capabilites=capabilites, + timestamp=self.buffer_timestamp, + duration=duration) + + self.buffer_timestamp += duration if self.audio.emit_data(buffer_).get(): return num_frames diff --git a/mopidy/backends/stream/__init__.py b/mopidy/backends/stream/__init__.py new file mode 100644 index 00000000..82755540 --- /dev/null +++ b/mopidy/backends/stream/__init__.py @@ -0,0 +1,23 @@ +"""A backend for playing music for streaming music. + +This backend will handle streaming of URIs in +:attr:`mopidy.settings.STREAM_PROTOCOLS` assuming the right plugins are +installed. + +**Issues:** + +https://github.com/mopidy/mopidy/issues?labels=Stream+backend + +**Dependencies:** + +- None + +**Settings:** + +- :attr:`mopidy.settings.STREAM_PROTOCOLS` +""" + +from __future__ import unicode_literals + +# flake8: noqa +from .actor import StreamBackend diff --git a/mopidy/backends/stream/actor.py b/mopidy/backends/stream/actor.py new file mode 100644 index 00000000..f80ac7a9 --- /dev/null +++ b/mopidy/backends/stream/actor.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals + +import logging +import urlparse + +import pykka + +from mopidy import audio as audio_lib, settings +from mopidy.backends import base +from mopidy.models import Track + +logger = logging.getLogger('mopidy.backends.stream') + + +class StreamBackend(pykka.ThreadingActor, base.Backend): + def __init__(self, audio): + super(StreamBackend, self).__init__() + + self.library = StreamLibraryProvider(backend=self) + self.playback = base.BasePlaybackProvider(audio=audio, backend=self) + self.playlists = None + + self.uri_schemes = audio_lib.supported_uri_schemes( + settings.STREAM_PROTOCOLS) + + +# TODO: Should we consider letting lookup know how to expand common playlist +# formats (m3u, pls, etc) for http(s) URIs? +class StreamLibraryProvider(base.BaseLibraryProvider): + def lookup(self, uri): + if urlparse.urlsplit(uri).scheme not in self.backend.uri_schemes: + return [] + # TODO: actually lookup the stream metadata by getting tags in same + # way as we do for updating the local library with mopidy.scanner + # Note that we would only want the stream metadata at this stage, + # not the currently playing track's. + return [Track(uri=uri, name=uri)] diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 39a1e99c..e4be7ce8 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -41,7 +41,7 @@ class LibraryController(object): query = query or kwargs futures = [ b.library.find_exact(**query) for b in self.backends.with_library] - return pykka.get_all(futures) + return [result for result in pykka.get_all(futures) if result] def lookup(self, uri): """ @@ -101,4 +101,4 @@ class LibraryController(object): query = query or kwargs futures = [ b.library.search(**query) for b in self.backends.with_library] - return pykka.get_all(futures) + return [result for result in pykka.get_all(futures) if result] diff --git a/mopidy/scanner.py b/mopidy/scanner.py index 0b10d061..9f8c12f7 100644 --- a/mopidy/scanner.py +++ b/mopidy/scanner.py @@ -79,12 +79,15 @@ def main(): def parse_options(): parser = optparse.OptionParser( version='Mopidy %s' % versioning.get_version()) + # NOTE Python 2.6: To support Python versions < 2.6.2rc1 we must use + # bytestrings for the first argument to ``add_option`` + # See https://github.com/mopidy/mopidy/issues/302 for details parser.add_option( - '-q', '--quiet', + b'-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') parser.add_option( - '-v', '--verbose', + b'-v', '--verbose', action='count', default=1, dest='verbosity_level', help='more output (debug level)') return parser.parse_args(args=mopidy_args)[0] @@ -96,9 +99,13 @@ def translator(data): artist_kwargs = {} track_kwargs = {} + # NOTE: kwargs are explicitly made bytestrings to work on Python + # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for + # details. + def _retrieve(source_key, target_key, target): if source_key in data: - target[target_key] = data[source_key] + target[str(target_key)] = data[source_key] _retrieve(gst.TAG_ALBUM, 'name', album_kwargs) _retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs) @@ -111,7 +118,7 @@ def translator(data): except ValueError: pass # Ignore invalid dates else: - track_kwargs['date'] = date.isoformat() + track_kwargs[b'date'] = date.isoformat() _retrieve(gst.TAG_TITLE, 'name', track_kwargs) _retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs) @@ -125,12 +132,12 @@ def translator(data): 'musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs) if albumartist_kwargs: - album_kwargs['artists'] = [Artist(**albumartist_kwargs)] + album_kwargs[b'artists'] = [Artist(**albumartist_kwargs)] - track_kwargs['uri'] = data['uri'] - track_kwargs['length'] = data[gst.TAG_DURATION] - track_kwargs['album'] = Album(**album_kwargs) - track_kwargs['artists'] = [Artist(**artist_kwargs)] + track_kwargs[b'uri'] = data['uri'] + track_kwargs[b'length'] = data[gst.TAG_DURATION] + track_kwargs[b'album'] = Album(**album_kwargs) + track_kwargs[b'artists'] = [Artist(**artist_kwargs)] return Track(**track_kwargs) diff --git a/mopidy/settings.py b/mopidy/settings.py index c2081e27..fd3dfd6f 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -20,10 +20,12 @@ from __future__ import unicode_literals #: BACKENDS = ( #: u'mopidy.backends.local.LocalBackend', #: u'mopidy.backends.spotify.SpotifyBackend', +#: u'mopidy.backends.spotify.StreamBackend', #: ) BACKENDS = ( 'mopidy.backends.local.LocalBackend', 'mopidy.backends.spotify.SpotifyBackend', + 'mopidy.backends.stream.StreamBackend', ) #: The log format used for informational logging. @@ -286,7 +288,7 @@ SPOTIFY_PROXY_USERNAME = None #: Spotify proxy password. #: -#: Used by :mod:`mopidy.backends.spotify` +#: Used by :mod:`mopidy.backends.spotify`. #: #: Default:: #: @@ -295,9 +297,32 @@ SPOTIFY_PROXY_PASSWORD = None #: Max number of seconds to wait for Spotify operations to complete. #: -#: Used by :mod:`mopidy.backends.spotify` +#: Used by :mod:`mopidy.backends.spotify`. #: #: Default:: #: #: SPOTIFY_TIMEOUT = 10 SPOTIFY_TIMEOUT = 10 + +#: Whitelist of URIs to support streaming from. +#: +#: Used by :mod:`mopidy.backends.stream`. +#: +#: Default:: +#: +#: STREAM_PROTOCOLS = ( +#: u'http', +#: u'https', +#: u'mms', +#: u'rtmp', +#: u'rtmps', +#: u'rtsp', +#: ) +STREAM_PROTOCOLS = ( + 'http', + 'https', + 'mms', + 'rtmp', + 'rtmps', + 'rtsp', +) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index c4fa0ce2..7d988a90 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -120,7 +120,7 @@ def find_files(path): if not os.path.basename(path).startswith(b'.'): yield path else: - for dirpath, dirnames, filenames in os.walk(path): + for dirpath, dirnames, filenames in os.walk(path, followlinks=True): for dirname in dirnames: if dirname.startswith(b'.'): # Skip hidden folders by modifying dirnames inplace diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 5edf287e..6be8937c 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -101,7 +101,7 @@ class DebugThread(threading.Thread): stack = ''.join(traceback.format_stack(frame)) logger.debug( 'Current state of %s (%s):\n%s', - threads[ident], ident, stack) + threads.get(ident, '?'), ident, stack) del frame self.event.clear() diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 6eb462ce..8ae61e5b 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -142,7 +142,13 @@ def validate_settings(defaults, settings): 'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH', } - list_of_one_or_more = [ + must_be_iterable = [ + 'BACKENDS', + 'FRONTENDS', + 'STREAM_PROTOCOLS', + ] + + must_have_value_set = [ 'BACKENDS', 'FRONTENDS', ] @@ -171,13 +177,13 @@ def validate_settings(defaults, settings): 'Deprecated setting, please set the value via the GStreamer ' 'bin in OUTPUT.') - elif setting in list_of_one_or_more: - if not hasattr(value, '__iter__'): - errors[setting] = ( - 'Must be a tuple. ' - "Remember the comma after single values: (u'value',)") - if not value: - errors[setting] = 'Must contain at least one value.' + elif setting in must_be_iterable and not hasattr(value, '__iter__'): + errors[setting] = ( + 'Must be a tuple. ' + "Remember the comma after single values: (u'value',)") + + elif setting in must_have_value_set and not value: + errors[setting] = 'Must be set.' elif setting not in defaults and not setting.startswith('CUSTOM_'): errors[setting] = 'Unknown setting.' diff --git a/tests/core/library_test.py b/tests/core/library_test.py index 32e618d2..e01696c7 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -90,6 +90,22 @@ class CoreLibraryTest(unittest.TestCase): self.library1.find_exact.assert_called_once_with(any=['a']) self.library2.find_exact.assert_called_once_with(any=['a']) + def test_find_exact_filters_out_none(self): + track1 = Track(uri='dummy1:a') + result1 = SearchResult(tracks=[track1]) + + self.library1.find_exact().get.return_value = result1 + self.library1.find_exact.reset_mock() + self.library2.find_exact().get.return_value = None + self.library2.find_exact.reset_mock() + + result = self.core.library.find_exact(any=['a']) + + self.assertIn(result1, result) + self.assertNotIn(None, result) + self.library1.find_exact.assert_called_once_with(any=['a']) + self.library2.find_exact.assert_called_once_with(any=['a']) + def test_find_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') @@ -126,6 +142,22 @@ class CoreLibraryTest(unittest.TestCase): self.library1.search.assert_called_once_with(any=['a']) self.library2.search.assert_called_once_with(any=['a']) + def test_search_filters_out_none(self): + track1 = Track(uri='dummy1:a') + result1 = SearchResult(tracks=[track1]) + + self.library1.search().get.return_value = result1 + self.library1.search.reset_mock() + self.library2.search().get.return_value = None + self.library2.search.reset_mock() + + result = self.core.library.search(any=['a']) + + self.assertIn(result1, result) + self.assertNotIn(None, result) + self.library1.search.assert_called_once_with(any=['a']) + self.library2.search.assert_called_once_with(any=['a']) + def test_search_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') diff --git a/tests/scanner_test.py b/tests/scanner_test.py index 92e9a269..d8466e26 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -32,36 +32,40 @@ class TranslatorTest(unittest.TestCase): 'musicbrainz-albumartistid': 'mbalbumartistid', } + # NOTE: kwargs are explicitly made bytestrings to work on Python + # 2.6.0/2.6.1. See https://github.com/mopidy/mopidy/issues/302 for + # details. + self.album = { - 'name': 'albumname', - 'num_tracks': 2, - 'musicbrainz_id': 'mbalbumid', + b'name': 'albumname', + b'num_tracks': 2, + b'musicbrainz_id': 'mbalbumid', } self.artist = { - 'name': 'name', - 'musicbrainz_id': 'mbartistid', + b'name': 'name', + b'musicbrainz_id': 'mbartistid', } self.albumartist = { - 'name': 'albumartistname', - 'musicbrainz_id': 'mbalbumartistid', + b'name': 'albumartistname', + b'musicbrainz_id': 'mbalbumartistid', } self.track = { - 'uri': 'uri', - 'name': 'trackname', - 'date': '2006-01-01', - 'track_no': 1, - 'length': 4531, - 'musicbrainz_id': 'mbtrackid', + b'uri': 'uri', + b'name': 'trackname', + b'date': '2006-01-01', + b'track_no': 1, + b'length': 4531, + b'musicbrainz_id': 'mbtrackid', } def build_track(self): if self.albumartist: - self.album['artists'] = [Artist(**self.albumartist)] - self.track['album'] = Album(**self.album) - self.track['artists'] = [Artist(**self.artist)] + self.album[b'artists'] = [Artist(**self.albumartist)] + self.track[b'album'] = Album(**self.album) + self.track[b'artists'] = [Artist(**self.artist)] return Track(**self.track) def check(self): diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 1dcac1bb..51f0d89c 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -79,13 +79,13 @@ class ValidateSettingsTest(unittest.TestCase): result = setting_utils.validate_settings( self.defaults, {'FRONTENDS': []}) self.assertEqual( - result['FRONTENDS'], 'Must contain at least one value.') + result['FRONTENDS'], 'Must be set.') def test_empty_backends_list_returns_error(self): result = setting_utils.validate_settings( self.defaults, {'BACKENDS': []}) self.assertEqual( - result['BACKENDS'], 'Must contain at least one value.') + result['BACKENDS'], 'Must be set.') def test_noniterable_multivalue_setting_returns_error(self): result = setting_utils.validate_settings(