diff --git a/docs/changelog.rst b/docs/changelog.rst index b976c169..3b0336a0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,35 @@ Changelog This changelog is used to track all major changes to Mopidy. +v1.0.1 (2015-04-23) +=================== + +Bug fix release. + +- Core: Make the new history controller available for use. (Fixes: :js:`6`) + +- Audio: Software volume control has been reworked to greatly reduce the delay + between changing the volume and the change taking effect. (Fixes: + :issue:`1097`, PR: :issue:`1101`) + +- Audio: As a side effect of the previous bug fix, software volume is no longer + tied to the PulseAudio application volume when using ``pulsesink``. This + behavior was confusing for many users and doesn't work well with the plans + for multiple outputs. + +- Audio: Update scanner to decode all media it finds. This should fix cases + where the scanner hangs on non-audio files like video. The scanner will now + also let us know if we found any decodeable audio. (Fixes: :issue:`726`, PR: + issue:`1124`) + +- HTTP: Fix threading bug that would cause duplicate delivery of WS messages. + (PR: :issue:`1127`) + +- MPD: Fix case where a playlist that is present in both browse and as a listed + playlist breaks the MPD frontend protocol output. (Fixes :issue:`1120`, PR: + :issue:`1142`) + + v1.0.0 (2015-03-25) =================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 388bb9f0..8dff0012 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -30,4 +30,4 @@ except ImportError: warnings.filterwarnings('ignore', 'could not open display') -__version__ = '1.0.0' +__version__ = '1.0.1' diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index b4c78ecb..e0a7892a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -8,7 +8,7 @@ import gobject import pygst pygst.require('0.10') import gst # noqa -import gst.pbutils +import gst.pbutils # noqa import pykka @@ -34,25 +34,6 @@ _GST_STATE_MAPPING = { gst.STATE_PAUSED: PlaybackState.PAUSED, gst.STATE_NULL: PlaybackState.STOPPED} -MB = 1 << 20 - -# GST_PLAY_FLAG_VIDEO (1<<0) -# GST_PLAY_FLAG_AUDIO (1<<1) -# GST_PLAY_FLAG_TEXT (1<<2) -# GST_PLAY_FLAG_VIS (1<<3) -# GST_PLAY_FLAG_SOFT_VOLUME (1<<4) -# GST_PLAY_FLAG_NATIVE_AUDIO (1<<5) -# GST_PLAY_FLAG_NATIVE_VIDEO (1<<6) -# GST_PLAY_FLAG_DOWNLOAD (1<<7) -# GST_PLAY_FLAG_BUFFERING (1<<8) -# GST_PLAY_FLAG_DEINTERLACE (1<<9) -# GST_PLAY_FLAG_SOFT_COLORBALANCE (1<<10) - -# Default flags to use for playbin: AUDIO, SOFT_VOLUME -# TODO: consider removing soft volume when we do multi outputs and handling it -# ourselves. -PLAYBIN_FLAGS = (1 << 1) | (1 << 4) - class _Signals(object): """Helper for tracking gobject signal registrations""" @@ -114,7 +95,7 @@ class _Appsrc(object): source.set_property('caps', self._caps) source.set_property('format', b'time') source.set_property('stream-type', b'seekable') - source.set_property('max-bytes', 1 * MB) + source.set_property('max-bytes', 1 << 20) # 1MB source.set_property('min-percent', 50) if self._need_data_callback: @@ -152,26 +133,12 @@ class _Appsrc(object): # TODO: expose this as a property on audio when #790 gets further along. class _Outputs(gst.Bin): def __init__(self): - gst.Bin.__init__(self) + gst.Bin.__init__(self, 'outputs') self._tee = gst.element_factory_make('tee') self.add(self._tee) - # Queue element to buy us time between the about to finish event and - # the actual switch, i.e. about to switch can block for longer thanks - # to this queue. - # TODO: make the min-max values a setting? - # TODO: this does not belong in this class. - queue = gst.element_factory_make('queue') - queue.set_property('max-size-buffers', 0) - queue.set_property('max-size-bytes', 0) - queue.set_property('max-size-time', 5 * gst.SECOND) - queue.set_property('min-threshold-time', 3 * gst.SECOND) - self.add(queue) - - queue.link(self._tee) - - ghost_pad = gst.GhostPad('sink', queue.get_pad('sink')) + ghost_pad = gst.GhostPad('sink', self._tee.get_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee @@ -195,7 +162,9 @@ class _Outputs(gst.Bin): def _add(self, element): # All tee branches need a queue in front of them. + # But keep the queue short so the volume change isn't to slow: queue = gst.element_factory_make('queue') + queue.set_property('max-size-buffers', 5) self.add(element) self.add(queue) queue.link(element) @@ -214,10 +183,6 @@ class SoftwareMixer(object): def setup(self, element, mixer_ref): self._element = element - - self._signals.connect(element, 'notify::volume', self._volume_changed) - self._signals.connect(element, 'notify::mute', self._mute_changed) - self._mixer.setup(mixer_ref) def teardown(self): @@ -229,24 +194,16 @@ class SoftwareMixer(object): def set_volume(self, volume): self._element.set_property('volume', volume / 100.0) + self._mixer.trigger_volume_changed(volume) def get_mute(self): return self._element.get_property('mute') def set_mute(self, mute): - return self._element.set_property('mute', bool(mute)) - - def _volume_changed(self, element, property_): - old_volume, self._last_volume = self._last_volume, self.get_volume() - if old_volume != self._last_volume: - gst_logger.debug('Notify volume: %s', self._last_volume / 100.0) - self._mixer.trigger_volume_changed(self._last_volume) - - def _mute_changed(self, element, property_): - old_mute, self._last_mute = self._last_mute, self.get_mute() - if old_mute != self._last_mute: - gst_logger.debug('Notify mute: %s', self._last_mute) - self._mixer.trigger_mute_changed(self._last_mute) + result = self._element.set_property('mute', bool(mute)) + if result: + self._mixer.trigger_mute_changed(bool(mute)) + return result class _Handler(object): @@ -451,8 +408,8 @@ class Audio(pykka.ThreadingActor): try: self._setup_preferences() self._setup_playbin() - self._setup_output() - self._setup_mixer() + self._setup_outputs() + self._setup_audio_sink() except gobject.GError as ex: logger.exception(ex) process.exit_process() @@ -472,11 +429,11 @@ class Audio(pykka.ThreadingActor): def _setup_playbin(self): playbin = gst.element_factory_make('playbin2') - playbin.set_property('flags', PLAYBIN_FLAGS) + playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... - playbin.set_property('buffer-size', 2 * 1024 * 1024) - playbin.set_property('buffer-duration', 2 * gst.SECOND) + playbin.set_property('buffer-size', 5 << 20) # 5MB + playbin.set_property('buffer-duration', 5 * gst.SECOND) self._signals.connect(playbin, 'source-setup', self._on_source_setup) self._signals.connect(playbin, 'about-to-finish', @@ -492,7 +449,7 @@ class Audio(pykka.ThreadingActor): self._signals.disconnect(self._playbin, 'source-setup') self._playbin.set_state(gst.STATE_NULL) - def _setup_output(self): + def _setup_outputs(self): # We don't want to use outputs for regular testing, so just install # an unsynced fakesink when someone asks for a 'testoutput'. if self._config['audio']['output'] == 'testoutput': @@ -505,11 +462,36 @@ class Audio(pykka.ThreadingActor): process.exit_process() # TODO: move this up the chain self._handler.setup_event_handling(self._outputs.get_pad('sink')) - self._playbin.set_property('audio-sink', self._outputs) - def _setup_mixer(self): + def _setup_audio_sink(self): + audio_sink = gst.Bin('audio-sink') + + # Queue element to buy us time between the about to finish event and + # the actual switch, i.e. about to switch can block for longer thanks + # to this queue. + # TODO: make the min-max values a setting? + queue = gst.element_factory_make('queue') + queue.set_property('max-size-buffers', 0) + queue.set_property('max-size-bytes', 0) + queue.set_property('max-size-time', 3 * gst.SECOND) + queue.set_property('min-threshold-time', 1 * gst.SECOND) + + audio_sink.add(queue) + audio_sink.add(self._outputs) + if self.mixer: - self.mixer.setup(self._playbin, self.actor_ref.proxy().mixer) + volume = gst.element_factory_make('volume') + audio_sink.add(volume) + queue.link(volume) + volume.link(self._outputs) + self.mixer.setup(volume, self.actor_ref.proxy().mixer) + else: + queue.link(self._outputs) + + ghost_pad = gst.GhostPad('sink', queue.get_pad('sink')) + audio_sink.add_pad(ghost_pad) + + self._playbin.set_property('audio-sink', audio_sink) def _teardown_mixer(self): if self.mixer: diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 3880d91a..d1e83407 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -1,11 +1,12 @@ -from __future__ import absolute_import, division, unicode_literals +from __future__ import ( + absolute_import, division, print_function, unicode_literals) import collections import pygst pygst.require('0.10') import gst # noqa -import gst.pbutils +import gst.pbutils # noqa from mopidy import exceptions from mopidy.audio import utils @@ -14,7 +15,7 @@ from mopidy.utils import encoding _missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description _Result = collections.namedtuple( - 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime')) + 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) _RAW_AUDIO = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') @@ -51,14 +52,14 @@ class Scanner(object): try: _start_pipeline(pipeline) - tags, mime = _process(pipeline, self._timeout_ms) + tags, mime, have_audio = _process(pipeline, self._timeout_ms) duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: pipeline.set_state(gst.STATE_NULL) del pipeline - return _Result(uri, tags, duration, seekable, mime) + return _Result(uri, tags, duration, seekable, mime, have_audio) # Turns out it's _much_ faster to just create a new pipeline for every as @@ -70,30 +71,38 @@ def _setup_pipeline(uri, proxy_config=None): typefind = gst.element_factory_make('typefind') decodebin = gst.element_factory_make('decodebin2') - sink = gst.element_factory_make('fakesink') pipeline = gst.element_factory_make('pipeline') - pipeline.add_many(src, typefind, decodebin, sink) + pipeline.add_many(src, typefind, decodebin) gst.element_link_many(src, typefind, decodebin) if proxy_config: utils.setup_proxy(src, proxy_config) - decodebin.set_property('caps', _RAW_AUDIO) - decodebin.connect('pad-added', _pad_added, sink) typefind.connect('have-type', _have_type, decodebin) + decodebin.connect('pad-added', _pad_added, pipeline) return pipeline def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) - msg = gst.message_new_application(element, caps.get_structure(0)) - element.get_bus().post(msg) + struct = gst.Structure('have-type') + struct['caps'] = caps.get_structure(0) + element.get_bus().post(gst.message_new_application(element, struct)) -def _pad_added(element, pad, sink): - return pad.link(sink.get_pad('sink')) +def _pad_added(element, pad, pipeline): + sink = gst.element_factory_make('fakesink') + sink.set_property('sync', False) + + pipeline.add(sink) + sink.sync_state_with_parent() + pad.link(sink.get_pad('sink')) + + if pad.get_caps().is_subset(_RAW_AUDIO): + struct = gst.Structure('have-audio') + element.get_bus().post(gst.message_new_application(element, struct)) def _start_pipeline(pipeline): @@ -123,7 +132,7 @@ def _process(pipeline, timeout_ms): clock = pipeline.get_clock() bus = pipeline.get_bus() timeout = timeout_ms * gst.MSECOND - tags, mime, missing_description = {}, None, None + tags, mime, have_audio, missing_description = {}, None, False, None types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) @@ -139,19 +148,22 @@ def _process(pipeline, timeout_ms): missing_description = encoding.locale_decode( _missing_plugin_desc(message)) elif message.type == gst.MESSAGE_APPLICATION: - mime = message.structure.get_name() - if mime.startswith('text/') or mime == 'application/xml': - return tags, mime + if message.structure.get_name() == 'have-type': + mime = message.structure['caps'].get_name() + if mime.startswith('text/') or mime == 'application/xml': + return tags, mime, have_audio + elif message.structure.get_name() == 'have-audio': + have_audio = True elif message.type == gst.MESSAGE_ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_description: error = '%s (%s)' % (missing_description, error) raise exceptions.ScannerError(error) elif message.type == gst.MESSAGE_EOS: - return tags, mime + return tags, mime, have_audio elif message.type == gst.MESSAGE_ASYNC_DONE: if message.src == pipeline: - return tags, mime + return tags, mime, have_audio elif message.type == gst.MESSAGE_TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. @@ -160,3 +172,28 @@ def _process(pipeline, timeout_ms): timeout -= clock.get_time() - start raise exceptions.ScannerError('Timeout after %dms' % timeout_ms) + + +if __name__ == '__main__': + import os + import sys + + import gobject + + from mopidy.utils import path + + gobject.threads_init() + + scanner = Scanner(5000) + for uri in sys.argv[1:]: + if not gst.uri_is_valid(uri): + uri = path.path_to_uri(os.path.abspath(uri)) + try: + result = scanner.scan(uri) + for key in ('uri', 'mime', 'duration', 'playable', 'seekable'): + print('%-20s %s' % (key, getattr(result, key))) + print('tags') + for tag, value in result.tags.items(): + print('%-20s %s' % (tag, value)) + except exceptions.ScannerError as error: + print('%s: %s' % (uri, error)) diff --git a/mopidy/core/history.py b/mopidy/core/history.py index f0d5e9d4..ae697e8e 100644 --- a/mopidy/core/history.py +++ b/mopidy/core/history.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) class HistoryController(object): + pykka_traversable = True def __init__(self): self._history = [] diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py index a5baf992..4f4b5988 100644 --- a/mopidy/http/handlers.py +++ b/mopidy/http/handlers.py @@ -5,6 +5,7 @@ import os import socket import tornado.escape +import tornado.ioloop import tornado.web import tornado.websocket @@ -65,6 +66,19 @@ def make_jsonrpc_wrapper(core_actor): ) +def _send_broadcast(client, msg): + # We could check for client.ws_connection, but we don't really + # care why the broadcast failed, we just want the rest of them + # to succeed, so catch everything. + try: + client.write_message(msg) + except Exception as e: + error_msg = encoding.locale_decode(e) + logger.debug('Broadcast of WebSocket message to %s failed: %s', + client.request.remote_ip, error_msg) + # TODO: should this do the same cleanup as the on_message code? + + class WebSocketHandler(tornado.websocket.WebSocketHandler): # XXX This set is shared by all WebSocketHandler objects. This isn't @@ -74,17 +88,12 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler): @classmethod def broadcast(cls, msg): + # This can be called from outside the Tornado ioloop, so we need to + # safely cross the thread boundary by adding a callback to the loop. + loop = tornado.ioloop.IOLoop.current() for client in cls.clients: - # We could check for client.ws_connection, but we don't really - # care why the broadcast failed, we just want the rest of them - # to succeed, so catch everything. - try: - client.write_message(msg) - except Exception as e: - error_msg = encoding.locale_decode(e) - logger.debug('Broadcast of WebSocket message to %s failed: %s', - client.request.remote_ip, error_msg) - # TODO: should this do the same cleanup as the on_message code? + # One callback per client to keep time we hold up the loop short + loop.add_callback(_send_broadcast, client, msg) def initialize(self, core): self.jsonrpc = make_jsonrpc_wrapper(core) diff --git a/mopidy/local/commands.py b/mopidy/local/commands.py index af8b0025..4383decb 100644 --- a/mopidy/local/commands.py +++ b/mopidy/local/commands.py @@ -135,7 +135,9 @@ class ScanCommand(commands.Command): file_uri = path.path_to_uri(os.path.join(media_dir, relpath)) result = scanner.scan(file_uri) tags, duration = result.tags, result.duration - if duration < MIN_DURATION_MS: + if not result.playable: + logger.warning('Failed %s: No audio found in file.', uri) + elif duration < MIN_DURATION_MS: logger.warning('Failed %s: Track shorter than %dms', uri, MIN_DURATION_MS) else: diff --git a/mopidy/mpd/uri_mapper.py b/mopidy/mpd/uri_mapper.py index 08c7f689..9e7ec2dd 100644 --- a/mopidy/mpd/uri_mapper.py +++ b/mopidy/mpd/uri_mapper.py @@ -2,8 +2,12 @@ from __future__ import absolute_import, unicode_literals import re +# TOOD: refactor this into a generic mapper that does not know about browse +# or playlists and then use one instance for each case? + class MpdUriMapper(object): + """ Maintains the mappings between uniquified MPD names and URIs. """ @@ -17,7 +21,8 @@ class MpdUriMapper(object): def __init__(self, core=None): self.core = core self._uri_from_name = {} - self._name_from_uri = {} + self._browse_name_from_uri = {} + self._playlist_name_from_uri = {} def _create_unique_name(self, name, uri): stripped_name = self._invalid_browse_chars.sub(' ', name) @@ -30,33 +35,37 @@ class MpdUriMapper(object): i += 1 return name - def insert(self, name, uri): + def insert(self, name, uri, playlist=False): """ Create a unique and MPD compatible name that maps to the given URI. """ name = self._create_unique_name(name, uri) self._uri_from_name[name] = uri - self._name_from_uri[uri] = name + if playlist: + self._playlist_name_from_uri[uri] = name + else: + self._browse_name_from_uri[uri] = name return name def uri_from_name(self, name): """ Return the uri for the given MPD name. """ - if name in self._uri_from_name: - return self._uri_from_name[name] + return self._uri_from_name.get(name) def refresh_playlists_mapping(self): """ Maintain map between playlists and unique playlist names to be used by MPD. """ - if self.core is not None: - for playlist_ref in self.core.playlists.as_list().get(): - if not playlist_ref.name: - continue - name = self._invalid_playlist_chars.sub('|', playlist_ref.name) - self.insert(name, playlist_ref.uri) + if self.core is None: + return + + for playlist_ref in self.core.playlists.as_list().get(): + if not playlist_ref.name: + continue + name = self._invalid_playlist_chars.sub('|', playlist_ref.name) + self.insert(name, playlist_ref.uri, playlist=True) def playlist_uri_from_name(self, name): """ @@ -70,6 +79,6 @@ class MpdUriMapper(object): """ Helper function to retrieve the unique MPD playlist name from its URI. """ - if uri not in self._name_from_uri: + if uri not in self._playlist_name_from_uri: self.refresh_playlists_mapping() - return self._name_from_uri[uri] + return self._playlist_name_from_uri[uri] diff --git a/mopidy/utils/deps.py b/mopidy/utils/deps.py index bc9f7c2f..aafede9d 100644 --- a/mopidy/utils/deps.py +++ b/mopidy/utils/deps.py @@ -5,12 +5,12 @@ import os import platform import sys +import pkg_resources + import pygst pygst.require('0.10') import gst # noqa -import pkg_resources - from mopidy.utils import formatting diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index fbc440de..b00646bc 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -6,12 +6,12 @@ import unittest import gobject gobject.threads_init() +import mock + import pygst pygst.require('0.10') import gst # noqa -import mock - import pykka from mopidy import audio diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index b2937a3f..f58b2202 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -16,8 +16,7 @@ from tests import path_to_data_dir class ScannerTest(unittest.TestCase): def setUp(self): # noqa: N802 self.errors = {} - self.tags = {} - self.durations = {} + self.result = {} def find(self, path): media_dir = path_to_data_dir(path) @@ -31,19 +30,17 @@ class ScannerTest(unittest.TestCase): uri = path_lib.path_to_uri(path) key = uri[len('file://'):] try: - result = scanner.scan(uri) - self.tags[key] = result.tags - self.durations[key] = result.duration + self.result[key] = scanner.scan(uri) except exceptions.ScannerError as error: self.errors[key] = error def check(self, name, key, value): name = path_to_data_dir(name) - self.assertEqual(self.tags[name][key], value) + self.assertEqual(self.result[name].tags[key], value) def test_tags_is_set(self): self.scan(self.find('scanner/simple')) - self.assert_(self.tags) + self.assert_(self.result.values()[0].tags) def test_errors_is_not_set(self): self.scan(self.find('scanner/simple')) @@ -52,10 +49,10 @@ class ScannerTest(unittest.TestCase): def test_duration_is_set(self): self.scan(self.find('scanner/simple')) - self.assertEqual( - self.durations[path_to_data_dir('scanner/simple/song1.mp3')], 4680) - self.assertEqual( - self.durations[path_to_data_dir('scanner/simple/song1.ogg')], 4680) + ogg = path_to_data_dir('scanner/simple/song1.ogg') + mp3 = path_to_data_dir('scanner/simple/song1.mp3') + self.assertEqual(self.result[mp3].duration, 4680) + self.assertEqual(self.result[ogg].duration, 4680) def test_artist_is_set(self): self.scan(self.find('scanner/simple')) @@ -78,17 +75,17 @@ class ScannerTest(unittest.TestCase): def test_other_media_is_ignored(self): self.scan(self.find('scanner/image')) - self.assert_(self.errors) + self.assertFalse(self.result.values()[0].playable) def test_log_file_that_gst_thinks_is_mpeg_1_is_ignored(self): self.scan([path_to_data_dir('scanner/example.log')]) - self.assertLess( - self.durations[path_to_data_dir('scanner/example.log')], 100) + log = path_to_data_dir('scanner/example.log') + self.assertLess(self.result[log].duration, 100) def test_empty_wav_file(self): self.scan([path_to_data_dir('scanner/empty.wav')]) - self.assertEqual( - self.durations[path_to_data_dir('scanner/empty.wav')], 0) + wav = path_to_data_dir('scanner/empty.wav') + self.assertEqual(self.result[wav].duration, 0) @unittest.SkipTest def test_song_without_time_is_handeled(self): diff --git a/tests/mpd/protocol/test_regression.py b/tests/mpd/protocol/test_regression.py index 6fb59afd..60a71ee8 100644 --- a/tests/mpd/protocol/test_regression.py +++ b/tests/mpd/protocol/test_regression.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import random -from mopidy.models import Track +from mopidy.models import Playlist, Ref, Track from tests.mpd import protocol @@ -175,3 +175,33 @@ class IssueGH137RegressionTest(protocol.BaseTestCase): u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"') self.assertInResponse('ACK [2@0] {list} Invalid unquoted character') + + +class IssueGH1120RegressionTest(protocol.BaseTestCase): + """ + The issue: https://github.com/mopidy/mopidy/issues/1120 + + How to reproduce: + + - A playlist must be in both browse results and playlists + - Call for instance ``lsinfo "/"`` to populate the cache with the + playlist name from the playlist backend. + - Call ``lsinfo "/dummy"`` to override the playlist name with the browse + name. + - Call ``lsinfo "/"`` and we now have an invalid name with ``/`` in it. + + """ + + def test(self): + self.backend.library.dummy_browse_result = { + 'dummy:/': [Ref.playlist(name='Top 100 tracks', uri='dummy:/1')], + } + self.backend.playlists.set_dummy_playlists([ + Playlist(name='Top 100 tracks', uri='dummy:/1'), + ]) + + response1 = self.send_request('lsinfo "/"') + self.send_request('lsinfo "/dummy"') + + response2 = self.send_request('lsinfo "/"') + self.assertEqual(response1, response2) diff --git a/tests/stream/test_library.py b/tests/stream/test_library.py index 93292376..65c59cb6 100644 --- a/tests/stream/test_library.py +++ b/tests/stream/test_library.py @@ -5,12 +5,12 @@ import unittest import gobject gobject.threads_init() +import mock + import pygst pygst.require('0.10') import gst # noqa: pygst magic is needed to import correct gst -import mock - from mopidy.models import Track from mopidy.stream import actor from mopidy.utils.path import path_to_uri diff --git a/tests/test_version.py b/tests/test_version.py index 932cc639..3d284121 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -55,5 +55,6 @@ class VersionTest(unittest.TestCase): self.assertVersionLess('0.19.2', '0.19.3') self.assertVersionLess('0.19.3', '0.19.4') self.assertVersionLess('0.19.4', '0.19.5') - self.assertVersionLess('0.19.5', __version__) - self.assertVersionLess(__version__, '1.0.1') + self.assertVersionLess('0.19.5', '1.0.0') + self.assertVersionLess('1.0.0', __version__) + self.assertVersionLess(__version__, '1.0.2') diff --git a/tests/utils/test_deps.py b/tests/utils/test_deps.py index 95f5b982..3b06973f 100644 --- a/tests/utils/test_deps.py +++ b/tests/utils/test_deps.py @@ -6,12 +6,12 @@ import unittest import mock +import pkg_resources + import pygst pygst.require('0.10') import gst # noqa -import pkg_resources - from mopidy.utils import deps