From cb19b2c48c1c59b0ecd7386f8e4d8ca6e0f43c65 Mon Sep 17 00:00:00 2001 From: Lasse Bigum Date: Sat, 7 Feb 2015 22:54:02 +0100 Subject: [PATCH 01/23] Allow 'none' as audio.mixer value To disable mixing altogether, you can now set the configuration value audio/mixer to 'none'. --- docs/changelog.rst | 7 ++ docs/config.rst | 2 + mopidy/commands.py | 14 +++- mopidy/core/mixer.py | 23 +++--- mopidy/mpd/protocol/audio_output.py | 19 +++-- mopidy/mpd/protocol/playback.py | 15 ++-- tests/core/test_listener.py | 3 + tests/core/test_mixer.py | 55 +++++++++++++++ tests/dummy_mixer.py | 4 ++ tests/mpd/protocol/__init__.py | 7 +- tests/mpd/protocol/test_audio_output.py | 93 +++++++++++++++++++++++++ tests/mpd/protocol/test_idle.py | 30 ++++++++ tests/mpd/protocol/test_playback.py | 16 +++++ tests/mpd/test_exceptions.py | 12 +++- 14 files changed, 274 insertions(+), 26 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ca36454e..1d2f520d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -57,6 +57,9 @@ v0.20.0 (UNRELEASED) - Add debug logging of unknown sections. (Fixes: :issue:`694`, PR: :issue:`1002`) +- Add support for configuring :confval:`audio/mixer` to ``none``. (Fixes: + :issue:`936`) + **Logging** - Add custom log level ``TRACE`` (numerical level 5), which can be used by @@ -114,6 +117,10 @@ v0.20.0 (UNRELEASED) - Switch the ``list`` command over to using :meth:`mopidy.core.LibraryController.get_distinct`. (Fixes: :issue:`913`) +- Add support for ``toggleoutput`` command. The ``mixrampdb`` and + ``mixrampdelay`` commands are now supported but throw a NotImplemented + exception. + **HTTP frontend** - Prevent race condition in webservice broadcast from breaking the server. diff --git a/docs/config.rst b/docs/config.rst index 69945ab8..46b15635 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -70,6 +70,8 @@ Audio configuration will affect the audio volume if you're streaming the audio from Mopidy through Shoutcast. + If you want to disable audio mixing set the value to ``none``. + If you want to use a hardware mixer, you need to install a Mopidy extension which integrates with your sound subsystem. E.g. for ALSA, install `Mopidy-ALSAMixer `_. diff --git a/mopidy/commands.py b/mopidy/commands.py index d9b4ce0e..5df8dd5a 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -276,7 +276,9 @@ class RootCommand(Command): exit_status_code = 0 try: - mixer = self.start_mixer(config, mixer_class) + mixer = None + if mixer_class is not None: + mixer = self.start_mixer(config, mixer_class) audio = self.start_audio(config, mixer) backends = self.start_backends(config, backend_classes, audio) core = self.start_core(mixer, backends, audio) @@ -297,7 +299,8 @@ class RootCommand(Command): self.stop_core() self.stop_backends(backend_classes) self.stop_audio() - self.stop_mixer(mixer_class) + if mixer_class is not None: + self.stop_mixer(mixer_class) process.stop_remaining_actors() return exit_status_code @@ -306,13 +309,18 @@ class RootCommand(Command): 'Available Mopidy mixers: %s', ', '.join(m.__name__ for m in mixer_classes) or 'none') + if config['audio']['mixer'] == 'none': + logger.debug('Mixer disabled') + return None + selected_mixers = [ m for m in mixer_classes if m.name == config['audio']['mixer']] if len(selected_mixers) != 1: logger.error( 'Did not find unique mixer "%s". Alternatives are: %s', config['audio']['mixer'], - ', '.join([m.name for m in mixer_classes])) + ', '.join([m.name for m in mixer_classes]) + ', none' or + 'none') process.exit_process() return selected_mixers[0] diff --git a/mopidy/core/mixer.py b/mopidy/core/mixer.py index 4d77f8bc..1f5ada9e 100644 --- a/mopidy/core/mixer.py +++ b/mopidy/core/mixer.py @@ -11,8 +11,6 @@ class MixerController(object): def __init__(self, mixer): self._mixer = mixer - self._volume = None - self._mute = False def get_volume(self): """Get the volume. @@ -27,12 +25,15 @@ class MixerController(object): def set_volume(self, volume): """Set the volume. - The volume is defined as an integer in range [0..100]. + The volume is defined as an integer in range [0..100] or :class:`None` + if the mixer is disabled. The volume scale is linear. """ - if self._mixer is not None: - self._mixer.set_volume(volume) + if self._mixer is None: + return False + else: + return self._mixer.set_volume(volume).get() def get_mute(self): """Get mute state. @@ -40,13 +41,19 @@ class MixerController(object): :class:`True` if muted, :class:`False` unmuted, :class:`None` if unknown. """ - if self._mixer is not None: + if self._mixer is None: + return False + else: return self._mixer.get_mute().get() def set_mute(self, mute): """Set mute state. :class:`True` to mute, :class:`False` to unmute. + + Returns :class:`True` if call is successful, otherwise :class:`False`. """ - if self._mixer is not None: - self._mixer.set_mute(bool(mute)) + if self._mixer is None: + return False + else: + return self._mixer.set_mute(bool(mute)).get() diff --git a/mopidy/mpd/protocol/audio_output.py b/mopidy/mpd/protocol/audio_output.py index 0152f852..6ffedcf1 100644 --- a/mopidy/mpd/protocol/audio_output.py +++ b/mopidy/mpd/protocol/audio_output.py @@ -13,7 +13,9 @@ def disableoutput(context, outputid): Turns an output off. """ if outputid == 0: - context.core.mixer.set_mute(False) + success = context.core.mixer.set_mute(False).get() + if success is False: + raise exceptions.MpdSystemError('problems disabling output') else: raise exceptions.MpdNoExistError('No such audio output') @@ -28,13 +30,14 @@ def enableoutput(context, outputid): Turns an output on. """ if outputid == 0: - context.core.mixer.set_mute(True) + success = context.core.mixer.set_mute(True).get() + if success is False: + raise exceptions.MpdSystemError('problems enabling output') else: raise exceptions.MpdNoExistError('No such audio output') -# TODO: implement and test -# @protocol.commands.add('toggleoutput', outputid=protocol.UINT) +@protocol.commands.add('toggleoutput', outputid=protocol.UINT) def toggleoutput(context, outputid): """ *musicpd.org, audio output section:* @@ -43,7 +46,13 @@ def toggleoutput(context, outputid): Turns an output on or off, depending on the current state. """ - pass + if outputid == 0: + mute_status = context.core.mixer.get_mute().get() + success = context.core.mixer.set_mute(not mute_status) + if success is False: + raise exceptions.MpdSystemError('problems toggling output') + else: + raise exceptions.MpdNoExistError('No such audio output') @protocol.commands.add('outputs') diff --git a/mopidy/mpd/protocol/playback.py b/mopidy/mpd/protocol/playback.py index f7856a03..4cf8b2e8 100644 --- a/mopidy/mpd/protocol/playback.py +++ b/mopidy/mpd/protocol/playback.py @@ -32,8 +32,7 @@ def crossfade(context, seconds): raise exceptions.MpdNotImplemented # TODO -# TODO: add at least reflection tests before adding NotImplemented version -# @protocol.commands.add('mixrampdb') +@protocol.commands.add('mixrampdb') def mixrampdb(context, decibels): """ *musicpd.org, playback section:* @@ -46,11 +45,10 @@ def mixrampdb(context, decibels): volume so use negative values, I prefer -17dB. In the absence of mixramp tags crossfading will be used. See http://sourceforge.net/projects/mixramp """ - pass + raise exceptions.MpdNotImplemented # TODO -# TODO: add at least reflection tests before adding NotImplemented version -# @protocol.commands.add('mixrampdelay', seconds=protocol.UINT) +@protocol.commands.add('mixrampdelay', seconds=protocol.UINT) def mixrampdelay(context, seconds): """ *musicpd.org, playback section:* @@ -61,7 +59,7 @@ def mixrampdelay(context, seconds): value of "nan" disables MixRamp overlapping and falls back to crossfading. """ - pass + raise exceptions.MpdNotImplemented # TODO @protocol.commands.add('next') @@ -397,7 +395,10 @@ def setvol(context, volume): - issues ``setvol 50`` without quotes around the argument. """ # NOTE: we use INT as clients can pass in +N etc. - context.core.mixer.set_volume(min(max(0, volume), 100)) + value = min(max(0, volume), 100) + success = context.core.mixer.set_volume(value).get() + if success is False: + raise exceptions.MpdSystemError('problems setting volume') @protocol.commands.add('single', state=protocol.BOOL) diff --git a/tests/core/test_listener.py b/tests/core/test_listener.py index 64003769..1338ec5e 100644 --- a/tests/core/test_listener.py +++ b/tests/core/test_listener.py @@ -57,3 +57,6 @@ class CoreListenerTest(unittest.TestCase): def test_listener_has_default_impl_for_seeked(self): self.listener.seeked(0) + + def test_listener_has_default_impl_for_current_metadata_changed(self): + self.listener.current_metadata_changed() diff --git a/tests/core/test_mixer.py b/tests/core/test_mixer.py index 80e6f7ef..6485f3e8 100644 --- a/tests/core/test_mixer.py +++ b/tests/core/test_mixer.py @@ -4,7 +4,10 @@ import unittest import mock +import pykka + from mopidy import core, mixer +from tests import dummy_mixer class CoreMixerTest(unittest.TestCase): @@ -33,3 +36,55 @@ class CoreMixerTest(unittest.TestCase): self.core.mixer.set_mute(True) self.mixer.set_mute.assert_called_once_with(True) + + +class CoreNoneMixerTest(unittest.TestCase): + def setUp(self): # noqa: N802 + self.core = core.Core(mixer=None, backends=[]) + + def test_get_volume_return_none(self): + self.assertEqual(self.core.mixer.get_volume(), None) + + def test_set_volume_return_false(self): + self.assertEqual(self.core.mixer.set_volume(30), False) + + def test_get_set_mute_return_proper_state(self): + self.assertEqual(self.core.mixer.set_mute(False), False) + self.assertEqual(self.core.mixer.get_mute(), False) + self.assertEqual(self.core.mixer.set_mute(True), False) + self.assertEqual(self.core.mixer.get_mute(), False) + + +@mock.patch.object(mixer.MixerListener, 'send') +class CoreMixerListenerTest(unittest.TestCase): + def setUp(self): # noqa: N802 + self.mixer = dummy_mixer.create_proxy() + self.core = core.Core(mixer=self.mixer, backends=[]) + + def tearDown(self): # noqa: N802 + pykka.ActorRegistry.stop_all() + + def test_forwards_mixer_volume_changed_event_to_frontends(self, send): + self.assertEqual(self.core.mixer.set_volume(volume=60), True) + self.assertEqual(send.call_args[0][0], 'volume_changed') + self.assertEqual(send.call_args[1]['volume'], 60) + + def test_forwards_mixer_mute_changed_event_to_frontends(self, send): + self.core.mixer.set_mute(mute=True) + + self.assertEqual(send.call_args[0][0], 'mute_changed') + self.assertEqual(send.call_args[1]['mute'], True) + + +@mock.patch.object(mixer.MixerListener, 'send') +class CoreNoneMixerListenerTest(unittest.TestCase): + def setUp(self): # noqa: N802 + self.core = core.Core(mixer=None, backends=[]) + + def test_forwards_mixer_volume_changed_event_to_frontends(self, send): + self.assertEqual(self.core.mixer.set_volume(volume=60), False) + self.assertEqual(send.call_count, 0) + + def test_forwards_mixer_mute_changed_event_to_frontends(self, send): + self.core.mixer.set_mute(mute=True) + self.assertEqual(send.call_count, 0) diff --git a/tests/dummy_mixer.py b/tests/dummy_mixer.py index f7d90b17..6defddba 100644 --- a/tests/dummy_mixer.py +++ b/tests/dummy_mixer.py @@ -21,9 +21,13 @@ class DummyMixer(pykka.ThreadingActor, mixer.Mixer): def set_volume(self, volume): self._volume = volume + self.trigger_volume_changed(volume=volume) + return True def get_mute(self): return self._mute def set_mute(self, mute): self._mute = mute + self.trigger_mute_changed(mute=mute) + return True diff --git a/tests/mpd/protocol/__init__.py b/tests/mpd/protocol/__init__.py index b07a5ba3..88e3567b 100644 --- a/tests/mpd/protocol/__init__.py +++ b/tests/mpd/protocol/__init__.py @@ -25,6 +25,8 @@ class MockConnection(mock.Mock): class BaseTestCase(unittest.TestCase): + enable_mixer = True + def get_config(self): return { 'mpd': { @@ -33,7 +35,10 @@ class BaseTestCase(unittest.TestCase): } def setUp(self): # noqa: N802 - self.mixer = dummy_mixer.create_proxy() + if self.enable_mixer: + self.mixer = dummy_mixer.create_proxy() + else: + self.mixer = None self.backend = dummy_backend.create_proxy() self.core = core.Core.start( mixer=self.mixer, backends=[self.backend]).proxy() diff --git a/tests/mpd/protocol/test_audio_output.py b/tests/mpd/protocol/test_audio_output.py index a86f24f0..322bf181 100644 --- a/tests/mpd/protocol/test_audio_output.py +++ b/tests/mpd/protocol/test_audio_output.py @@ -4,6 +4,7 @@ from tests.mpd import protocol class AudioOutputHandlerTest(protocol.BaseTestCase): + def test_enableoutput(self): self.core.mixer.set_mute(False) @@ -50,3 +51,95 @@ class AudioOutputHandlerTest(protocol.BaseTestCase): self.assertInResponse('outputname: Mute') self.assertInResponse('outputenabled: 1') self.assertInResponse('OK') + + def test_outputs_toggleoutput(self): + self.core.mixer.set_mute(False) + + self.send_request('toggleoutput "0"') + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 1') + self.assertInResponse('OK') + + self.send_request('toggleoutput "0"') + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 0') + self.assertInResponse('OK') + + self.send_request('toggleoutput "0"') + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 1') + self.assertInResponse('OK') + + def test_outputs_toggleoutput_unknown_outputid(self): + self.send_request('toggleoutput "7"') + + self.assertInResponse( + 'ACK [50@0] {toggleoutput} No such audio output') + + +class AudioOutputHandlerNoneMixerTest(protocol.BaseTestCase): + enable_mixer = False + + def test_enableoutput(self): + self.core.mixer.set_mute(False) + + self.send_request('enableoutput "0"') + self.assertInResponse( + 'ACK [52@0] {enableoutput} problems enabling output') + self.assertEqual(self.core.mixer.get_mute().get(), False) + + def test_disableoutput(self): + self.core.mixer.set_mute(True) + + self.send_request('disableoutput "0"') + self.assertInResponse( + 'ACK [52@0] {disableoutput} problems disabling output') + self.assertEqual(self.core.mixer.get_mute().get(), False) + + def test_outputs_when_unmuted(self): + self.core.mixer.set_mute(False) + + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 0') + self.assertInResponse('OK') + + def test_outputs_when_muted(self): + self.core.mixer.set_mute(True) + + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 0') + self.assertInResponse('OK') + + def test_outputs_toggleoutput(self): + self.core.mixer.set_mute(False) + + self.send_request('toggleoutput "0"') + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 0') + self.assertInResponse('OK') + + self.send_request('toggleoutput "0"') + self.send_request('outputs') + + self.assertInResponse('outputid: 0') + self.assertInResponse('outputname: Mute') + self.assertInResponse('outputenabled: 0') + self.assertInResponse('OK') diff --git a/tests/mpd/protocol/test_idle.py b/tests/mpd/protocol/test_idle.py index 0bd16992..e3c6ad38 100644 --- a/tests/mpd/protocol/test_idle.py +++ b/tests/mpd/protocol/test_idle.py @@ -50,6 +50,12 @@ class IdleHandlerTest(protocol.BaseTestCase): self.assertNoEvents() self.assertNoResponse() + def test_idle_output(self): + self.send_request('idle output') + self.assertEqualSubscriptions(['output']) + self.assertNoEvents() + self.assertNoResponse() + def test_idle_player_playlist(self): self.send_request('idle player playlist') self.assertEqualSubscriptions(['player', 'playlist']) @@ -102,6 +108,22 @@ class IdleHandlerTest(protocol.BaseTestCase): self.assertOnceInResponse('changed: player') self.assertOnceInResponse('OK') + def test_idle_then_output(self): + self.send_request('idle') + self.idle_event('output') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertOnceInResponse('changed: output') + self.assertOnceInResponse('OK') + + def test_idle_output_then_event_output(self): + self.send_request('idle output') + self.idle_event('output') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertOnceInResponse('changed: output') + self.assertOnceInResponse('OK') + def test_idle_player_then_noidle(self): self.send_request('idle player') self.send_request('noidle') @@ -206,3 +228,11 @@ class IdleHandlerTest(protocol.BaseTestCase): self.assertNotInResponse('changed: player') self.assertOnceInResponse('changed: playlist') self.assertOnceInResponse('OK') + + def test_output_then_idle_toggleoutput(self): + self.idle_event('output') + self.send_request('idle output') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertOnceInResponse('changed: output') + self.assertOnceInResponse('OK') diff --git a/tests/mpd/protocol/test_playback.py b/tests/mpd/protocol/test_playback.py index ea9c59ce..4f3d6d7a 100644 --- a/tests/mpd/protocol/test_playback.py +++ b/tests/mpd/protocol/test_playback.py @@ -150,6 +150,14 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase): self.assertInResponse('OK') self.assertInResponse('off') + def test_mixrampdb(self): + self.send_request('mixrampdb "10"') + self.assertInResponse('ACK [0@0] {mixrampdb} Not implemented') + + def test_mixrampdelay(self): + self.send_request('mixrampdelay "10"') + self.assertInResponse('ACK [0@0] {mixrampdelay} Not implemented') + @unittest.SkipTest def test_replay_gain_status_off(self): pass @@ -463,3 +471,11 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.send_request('stop') self.assertEqual(STOPPED, self.core.playback.state.get()) self.assertInResponse('OK') + + +class PlaybackOptionsHandlerNoneMixerTest(protocol.BaseTestCase): + enable_mixer = False + + def test_setvol_max_error(self): + self.send_request('setvol "100"') + self.assertInResponse('ACK [52@0] {setvol} problems setting volume') diff --git a/tests/mpd/test_exceptions.py b/tests/mpd/test_exceptions.py index d055ef7e..7bb64096 100644 --- a/tests/mpd/test_exceptions.py +++ b/tests/mpd/test_exceptions.py @@ -3,8 +3,8 @@ from __future__ import absolute_import, unicode_literals import unittest from mopidy.mpd.exceptions import ( - MpdAckError, MpdNoCommand, MpdNotImplemented, MpdPermissionError, - MpdSystemError, MpdUnknownCommand) + MpdAckError, MpdNoCommand, MpdNoExistError, MpdNotImplemented, + MpdPermissionError, MpdSystemError, MpdUnknownCommand) class MpdExceptionsTest(unittest.TestCase): @@ -61,3 +61,11 @@ class MpdExceptionsTest(unittest.TestCase): self.assertEqual( e.get_mpd_ack(), 'ACK [4@0] {foo} you don\'t have permission for "foo"') + + def test_mpd_noexist_error(self): + try: + raise MpdNoExistError(command='foo') + except MpdNoExistError as e: + self.assertEqual( + e.get_mpd_ack(), + 'ACK [50@0] {foo} ') From 20e95eac077688f54bd700ffee6f2a80d81068c0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 10 Mar 2015 18:34:49 +0100 Subject: [PATCH 02/23] docs: Fix rST syntax error --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ca36454e..9e3fb9d2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -74,7 +74,7 @@ v0.20.0 (UNRELEASED) - Add support for giving local libraries direct access to tags and duration. (Fixes: :issue:`967`) -- Add "--force" option for local scan (Fixes: :issue:'910', PR: :issue:'1010') +- Add "--force" option for local scan (Fixes: :issue:`910`, PR: :issue:`1010`) - Stop ignoring ``offset`` and ``limit`` in searches. (Fixes: :issue:`917`, PR: :issue:`949`) From 9a507e17df0c5f3487fac6cff356fbf8697982bf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2015 21:51:28 +0100 Subject: [PATCH 03/23] docs: Improve pointer to contribution page --- docs/authors.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/authors.rst b/docs/authors.rst index 1a0f21ed..90ec6f23 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -14,7 +14,7 @@ our Git repository. .. include:: ../AUTHORS -If you already enjoy Mopidy, or don't enjoy it and want to help us making -Mopidy better, the best way to do so is to contribute back to the community. -You can contribute code, documentation, tests, bug reports, or help other -users, spreading the word, etc. See :ref:`contributing` for a head start. +If want to help us making Mopidy better, the best way to do so is to contribute +back to the community, either through code, documentation, tests, bug reports, +or by helping other users, spreading the word, etc. See :ref:`contributing` for +a head start. From e655d3938455d02bd7559e310493f97d0b4db5ce Mon Sep 17 00:00:00 2001 From: Thomas Kemmer Date: Thu, 12 Mar 2015 11:43:27 +0100 Subject: [PATCH 04/23] Fix #1031: Add get_images() to local library. --- docs/changelog.rst | 3 +++ mopidy/local/__init__.py | 23 ++++++++++++++++++++++- mopidy/local/library.py | 5 +++++ tests/local/test_library.py | 29 ++++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e3fb9d2..64cc2ed0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -88,6 +88,9 @@ v0.20.0 (UNRELEASED) - Sort local playlists by name. (Fixes: :issue:`1026`, PR: :issue:`1028`) +- Add :meth:`mopidy.local.Library.get_images` for looking up images + for local URIs. (Fixes: :issue:`1031`, PR: :issue:`1032`) + **File scanner** - Improve error logging for scan code (Fixes: :issue:`856`, PR: :issue:`874`) diff --git a/mopidy/local/__init__.py b/mopidy/local/__init__.py index 97ed4a09..eecaa4a2 100644 --- a/mopidy/local/__init__.py +++ b/mopidy/local/__init__.py @@ -4,7 +4,7 @@ import logging import os import mopidy -from mopidy import config, ext +from mopidy import config, ext, models logger = logging.getLogger(__name__) @@ -101,6 +101,27 @@ class Library(object): """ return set() + def get_images(self, uris): + """ + Lookup the images for the given URIs. + + The default implementation will simply call :meth:`lookup` and + try and use the album art for any tracks returned. Most local + libraries should replace this with something smarter or simply + return an empty dictionary. + + :param list uris: list of URIs to find images for + :rtype: {uri: tuple of :class:`mopidy.models.Image`} + """ + result = {} + for uri in uris: + image_uris = set() + for track in self.lookup(uri): + if track.album and track.album.images: + image_uris.update(track.album.images) + result[uri] = [models.Image(uri=u) for u in image_uris] + return result + def load(self): """ (Re)load any tracks stored in memory, if any, otherwise just return diff --git a/mopidy/local/library.py b/mopidy/local/library.py index 90a54770..77c122bd 100644 --- a/mopidy/local/library.py +++ b/mopidy/local/library.py @@ -28,6 +28,11 @@ class LocalLibraryProvider(backend.LibraryProvider): return set() return self._library.get_distinct(field, query) + def get_images(self, uris): + if not self._library: + return {} + return self._library.get_images(uris) + def refresh(self, uri=None): if not self._library: return 0 diff --git a/tests/local/test_library.py b/tests/local/test_library.py index 6cc1992e..13ad9405 100644 --- a/tests/local/test_library.py +++ b/tests/local/test_library.py @@ -11,7 +11,7 @@ import pykka from mopidy import core from mopidy.local import actor, json -from mopidy.models import Album, Artist, Track +from mopidy.models import Album, Artist, Image, Track from tests import path_to_data_dir @@ -580,3 +580,30 @@ class LocalLibraryProviderTest(unittest.TestCase): with self.assertRaises(LookupError): self.library.search(any=['']) + + def test_default_get_images_impl_no_images(self): + result = self.library.get_images([track.uri for track in self.tracks]) + self.assertEqual(result, {track.uri: tuple() for track in self.tracks}) + + @mock.patch.object(json.JsonLibrary, 'lookup') + def test_default_get_images_impl_album_images(self, mock_lookup): + library = actor.LocalBackend(config=self.config, audio=None).library + + image = Image(uri='imageuri') + album = Album(images=[image.uri]) + track = Track(uri='trackuri', album=album) + mock_lookup.return_value = [track] + + result = library.get_images([track.uri]) + self.assertEqual(result, {track.uri: [image]}) + + @mock.patch.object(json.JsonLibrary, 'get_images') + def test_local_library_get_images(self, mock_get_images): + library = actor.LocalBackend(config=self.config, audio=None).library + + image = Image(uri='imageuri') + track = Track(uri='trackuri') + mock_get_images.return_value = {track.uri: [image]} + + result = library.get_images([track.uri]) + self.assertEqual(result, {track.uri: [image]}) From 40c7225cb75c940d0e0774c51f1c5e2bec4e88e2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 12 Mar 2015 22:11:33 +0100 Subject: [PATCH 05/23] local: Fix remainder display in local scan --- mopidy/local/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/local/commands.py b/mopidy/local/commands.py index 798c10f8..279fda13 100644 --- a/mopidy/local/commands.py +++ b/mopidy/local/commands.py @@ -177,6 +177,6 @@ class _Progress(object): logger.info('Scanned %d of %d files in %ds.', self.count, self.total, duration) else: - remainder = duration // self.count * (self.total - self.count) + remainder = duration / self.count * (self.total - self.count) logger.info('Scanned %d of %d files in %ds, ~%ds left.', self.count, self.total, duration, remainder) From f4e6956bb749045b35179f99357c551f44d0dfda Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 11 Mar 2015 22:58:41 +0100 Subject: [PATCH 06/23] audio: Catch missing plugins in scanner for better error messages --- mopidy/audio/scan.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 38b86437..c3eec941 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -5,11 +5,14 @@ import time import pygst pygst.require('0.10') import gst # noqa +import gst.pbutils from mopidy import exceptions from mopidy.audio import utils from mopidy.utils import encoding +_missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description + class Scanner(object): """ @@ -86,7 +89,11 @@ class Scanner(object): continue message = self._bus.pop() - if message.type == gst.MESSAGE_ERROR: + if message.type == gst.MESSAGE_ELEMENT: + if gst.pbutils.is_missing_plugin_message(message): + description = _missing_plugin_desc(message) + raise exceptions.ScannerError(description) + elif message.type == gst.MESSAGE_ERROR: raise exceptions.ScannerError( encoding.locale_decode(message.parse_error()[0])) elif message.type == gst.MESSAGE_EOS: From cee73b5501b7956ebc6cc5a5712fc31c3ff30523 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 11 Mar 2015 23:09:14 +0100 Subject: [PATCH 07/23] audio: Add support for checking seekable state in scanner Return type of scanner changed to a named tuple with (uri, tags, duration, seekable). This should help with #872 and the related "live" issues. Tests, local scan and stream metadata lookup have been updated to account for the changes. --- mopidy/audio/scan.py | 22 +++++++++++++++++----- mopidy/local/commands.py | 3 ++- mopidy/stream/actor.py | 6 +++--- tests/audio/test_scan.py | 6 +++--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c3eec941..d443b8bd 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, division, unicode_literals +import collections import time import pygst @@ -13,6 +14,9 @@ from mopidy.utils import encoding _missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description +Result = collections.namedtuple( + 'Result', ('uri', 'tags', 'duration', 'seekable')) + class Scanner(object): """ @@ -54,19 +58,22 @@ class Scanner(object): :param uri: URI of the resource to scan. :type event: string - :return: (tags, duration) pair. tags is a dictionary of lists for all - the tags we found and duration is the length of the URI in - milliseconds, or :class:`None` if the URI has no duration. + :return: A named tuple containing ``(uri, tags, duration, seekable)``. + ``tags`` is a dictionary of lists for all the tags we found. + ``duration`` is the length of the URI in milliseconds, or + :class:`None` if the URI has no duration. ``seekable`` is boolean + indicating if a seek would succeed. """ - tags, duration = None, None + tags, duration, seekable = None, None, None try: self._setup(uri) tags = self._collect() duration = self._query_duration() + seekable = self._query_seekable() finally: self._reset() - return tags, duration + return Result(uri, tags, duration, seekable) def _setup(self, uri): """Primes the pipeline for collection.""" @@ -123,3 +130,8 @@ class Scanner(object): return None else: return duration // gst.MSECOND + + def _query_seekable(self): + query = gst.query_new_seeking(gst.FORMAT_TIME) + self._pipe.query(query) + return query.parse_seeking()[1] diff --git a/mopidy/local/commands.py b/mopidy/local/commands.py index 279fda13..af8b0025 100644 --- a/mopidy/local/commands.py +++ b/mopidy/local/commands.py @@ -133,7 +133,8 @@ class ScanCommand(commands.Command): try: relpath = translator.local_track_uri_to_path(uri, media_dir) file_uri = path.path_to_uri(os.path.join(media_dir, relpath)) - tags, duration = scanner.scan(file_uri) + result = scanner.scan(file_uri) + tags, duration = result.tags, result.duration if duration < MIN_DURATION_MS: logger.warning('Failed %s: Track shorter than %dms', uri, MIN_DURATION_MS) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 58fd966a..47bfd58f 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -45,9 +45,9 @@ class StreamLibraryProvider(backend.LibraryProvider): return [Track(uri=uri)] try: - tags, duration = self._scanner.scan(uri) - track = utils.convert_tags_to_track(tags).copy( - uri=uri, length=duration) + result = self._scanner.scan(uri) + track = utils.convert_tags_to_track(result.tags).copy( + uri=uri, length=result.duration) except exceptions.ScannerError as e: logger.warning('Problem looking up %s: %s', uri, e) track = Track(uri=uri) diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 50ec8352..b2937a3f 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -31,9 +31,9 @@ class ScannerTest(unittest.TestCase): uri = path_lib.path_to_uri(path) key = uri[len('file://'):] try: - tags, duration = scanner.scan(uri) - self.tags[key] = tags - self.durations[key] = duration + result = scanner.scan(uri) + self.tags[key] = result.tags + self.durations[key] = result.duration except exceptions.ScannerError as error: self.errors[key] = error From ccd3753b30514a02245f923c39d77a008bb8c847 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 11 Mar 2015 23:14:24 +0100 Subject: [PATCH 08/23] audio: Switch to decodebin2 in scanner and handle our own sources This is needed to be able to put in our own typefind and catch playlists before they make it to the decoder. --- mopidy/audio/scan.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index d443b8bd..c4fca6ad 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -29,24 +29,21 @@ class Scanner(object): def __init__(self, timeout=1000, proxy_config=None): self._timeout_ms = timeout + self._proxy_config = proxy_config or {} sink = gst.element_factory_make('fakesink') - - audio_caps = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') + self._src = None def pad_added(src, pad): return pad.link(sink.get_pad('sink')) - def source_setup(element, source): - utils.setup_proxy(source, proxy_config or {}) - - self._uribin = gst.element_factory_make('uridecodebin') - self._uribin.set_property('caps', audio_caps) - self._uribin.connect('pad-added', pad_added) - self._uribin.connect('source-setup', source_setup) + audio_caps = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') + self._decodebin = gst.element_factory_make('decodebin2') + self._decodebin.set_property('caps', audio_caps) + self._decodebin.connect('pad-added', pad_added) self._pipe = gst.element_factory_make('pipeline') - self._pipe.add(self._uribin) + self._pipe.add(self._decodebin) self._pipe.add(sink) self._bus = self._pipe.get_bus() @@ -78,8 +75,16 @@ class Scanner(object): def _setup(self, uri): """Primes the pipeline for collection.""" self._pipe.set_state(gst.STATE_READY) - self._uribin.set_property(b'uri', uri) + + self._src = gst.element_make_from_uri(gst.URI_SRC, uri) + utils.setup_proxy(self._src, self._proxy_config) + + self._pipe.add(self._src) + self._src.sync_state_with_parent() + self._src.link(self._decodebin) + self._bus.set_flushing(False) + result = self._pipe.set_state(gst.STATE_PAUSED) if result == gst.STATE_CHANGE_NO_PREROLL: # Live sources don't pre-roll, so set to playing to get data. @@ -119,6 +124,9 @@ class Scanner(object): """Ensures we cleanup child elements and flush the bus.""" self._bus.set_flushing(True) self._pipe.set_state(gst.STATE_NULL) + self._src.unlink(self._decodebin) + self._pipe.remove(self._src) + self._src = None def _query_duration(self): try: From cd579ff7bbd36b8a69fad05080d6a661b043cc95 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 11 Mar 2015 23:20:30 +0100 Subject: [PATCH 09/23] audio: Going to NULL already handles the flushing for us --- mopidy/audio/scan.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c4fca6ad..84477def 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -47,7 +47,6 @@ class Scanner(object): self._pipe.add(sink) self._bus = self._pipe.get_bus() - self._bus.set_flushing(True) def scan(self, uri): """ @@ -83,8 +82,6 @@ class Scanner(object): self._src.sync_state_with_parent() self._src.link(self._decodebin) - self._bus.set_flushing(False) - result = self._pipe.set_state(gst.STATE_PAUSED) if result == gst.STATE_CHANGE_NO_PREROLL: # Live sources don't pre-roll, so set to playing to get data. @@ -121,8 +118,7 @@ class Scanner(object): raise exceptions.ScannerError('Timeout after %dms' % self._timeout_ms) def _reset(self): - """Ensures we cleanup child elements and flush the bus.""" - self._bus.set_flushing(True) + """Ensures we cleanup child elements.""" self._pipe.set_state(gst.STATE_NULL) self._src.unlink(self._decodebin) self._pipe.remove(self._src) From 24cceb69ebf2453b7f8cbd1f6c3126c39f2fd885 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 11 Mar 2015 23:21:41 +0100 Subject: [PATCH 10/23] audio: Going to ready is pointless in this code. --- mopidy/audio/scan.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 84477def..359f31cf 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -73,11 +73,8 @@ class Scanner(object): def _setup(self, uri): """Primes the pipeline for collection.""" - self._pipe.set_state(gst.STATE_READY) - self._src = gst.element_make_from_uri(gst.URI_SRC, uri) utils.setup_proxy(self._src, self._proxy_config) - self._pipe.add(self._src) self._src.sync_state_with_parent() self._src.link(self._decodebin) From c93eaad7ed53e9175a5b88636c1009ca10ae7804 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 12 Mar 2015 00:16:02 +0100 Subject: [PATCH 11/23] audio: Try and reuse source when we can --- mopidy/audio/scan.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 359f31cf..87e60076 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -73,11 +73,21 @@ class Scanner(object): def _setup(self, uri): """Primes the pipeline for collection.""" - self._src = gst.element_make_from_uri(gst.URI_SRC, uri) - utils.setup_proxy(self._src, self._proxy_config) - self._pipe.add(self._src) - self._src.sync_state_with_parent() - self._src.link(self._decodebin) + protocol = gst.uri_get_protocol(uri) + if self._src and protocol not in self._src.get_protocols(): + self._src.unlink(self._decodebin) + self._pipe.remove(self._src) + self._src = None + + if not self._src: + self._src = gst.element_make_from_uri(gst.URI_SRC, uri) + utils.setup_proxy(self._src, self._proxy_config) + self._pipe.add(self._src) + self._src.sync_state_with_parent() + self._src.link(self._decodebin) + + self._pipe.set_state(gst.STATE_READY) + self._src.set_uri(uri) result = self._pipe.set_state(gst.STATE_PAUSED) if result == gst.STATE_CHANGE_NO_PREROLL: @@ -115,11 +125,7 @@ class Scanner(object): raise exceptions.ScannerError('Timeout after %dms' % self._timeout_ms) def _reset(self): - """Ensures we cleanup child elements.""" self._pipe.set_state(gst.STATE_NULL) - self._src.unlink(self._decodebin) - self._pipe.remove(self._src) - self._src = None def _query_duration(self): try: From 837f2de62985232d9b7140e334ac70816d54933b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 12 Mar 2015 01:06:36 +0100 Subject: [PATCH 12/23] audio: Add typefinder to scanner and add mime to result This should allow us to move playlist handling out of GStreamer as we will short circuit for text/* and application/xml now. --- mopidy/audio/scan.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 87e60076..39cf172e 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -15,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')) + 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime')) class Scanner(object): @@ -37,15 +37,25 @@ class Scanner(object): def pad_added(src, pad): return pad.link(sink.get_pad('sink')) + def have_type(finder, probability, caps): + msg = gst.message_new_application(finder, caps.get_structure(0)) + finder.get_bus().post(msg) + + self._typefinder = gst.element_factory_make('typefind') + self._typefinder.connect('have-type', have_type) + audio_caps = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') self._decodebin = gst.element_factory_make('decodebin2') self._decodebin.set_property('caps', audio_caps) self._decodebin.connect('pad-added', pad_added) self._pipe = gst.element_factory_make('pipeline') + self._pipe.add(self._typefinder) self._pipe.add(self._decodebin) self._pipe.add(sink) + self._typefinder.link(self._decodebin) + self._bus = self._pipe.get_bus() def scan(self, uri): @@ -54,28 +64,29 @@ class Scanner(object): :param uri: URI of the resource to scan. :type event: string - :return: A named tuple containing ``(uri, tags, duration, seekable)``. + :return: A named tuple containing + ``(uri, tags, duration, seekable, mime)``. ``tags`` is a dictionary of lists for all the tags we found. ``duration`` is the length of the URI in milliseconds, or - :class:`None` if the URI has no duration. ``seekable`` is boolean + :class:`None` if the URI has no duration. ``seekable`` is boolean. indicating if a seek would succeed. """ - tags, duration, seekable = None, None, None + tags, duration, seekable, mime = None, None, None, None try: self._setup(uri) - tags = self._collect() + tags, mime = self._collect() duration = self._query_duration() seekable = self._query_seekable() finally: self._reset() - return Result(uri, tags, duration, seekable) + return Result(uri, tags, duration, seekable, mime) def _setup(self, uri): """Primes the pipeline for collection.""" protocol = gst.uri_get_protocol(uri) if self._src and protocol not in self._src.get_protocols(): - self._src.unlink(self._decodebin) + self._src.unlink(self._typefinder) self._pipe.remove(self._src) self._src = None @@ -83,8 +94,7 @@ class Scanner(object): self._src = gst.element_make_from_uri(gst.URI_SRC, uri) utils.setup_proxy(self._src, self._proxy_config) self._pipe.add(self._src) - self._src.sync_state_with_parent() - self._src.link(self._decodebin) + self._src.link(self._typefinder) self._pipe.set_state(gst.STATE_READY) self._src.set_uri(uri) @@ -98,7 +108,7 @@ class Scanner(object): """Polls for messages to collect data.""" start = time.time() timeout_s = self._timeout_ms / 1000.0 - tags = {} + tags, mime = {}, None while time.time() - start < timeout_s: if not self._bus.have_pending(): @@ -109,14 +119,18 @@ class Scanner(object): if gst.pbutils.is_missing_plugin_message(message): description = _missing_plugin_desc(message) raise exceptions.ScannerError(description) + elif message.type == gst.MESSAGE_APPLICATION: + mime = message.structure.get_name() + if mime.startswith('text/') or mime == 'application/xml': + return tags, mime elif message.type == gst.MESSAGE_ERROR: raise exceptions.ScannerError( encoding.locale_decode(message.parse_error()[0])) elif message.type == gst.MESSAGE_EOS: - return tags + return tags, mime elif message.type == gst.MESSAGE_ASYNC_DONE: if message.src == self._pipe: - return tags + return tags, mime elif message.type == gst.MESSAGE_TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. From 9c9d05be36616f528c9d79d7bc54e48d2e938283 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 12 Mar 2015 21:55:17 +0100 Subject: [PATCH 13/23] audio: Only warn about missing plugin on errors --- mopidy/audio/scan.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 39cf172e..ed8e9eb9 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -108,7 +108,7 @@ class Scanner(object): """Polls for messages to collect data.""" start = time.time() timeout_s = self._timeout_ms / 1000.0 - tags, mime = {}, None + tags, mime, missing_description = {}, None, None while time.time() - start < timeout_s: if not self._bus.have_pending(): @@ -117,15 +117,17 @@ class Scanner(object): if message.type == gst.MESSAGE_ELEMENT: if gst.pbutils.is_missing_plugin_message(message): - description = _missing_plugin_desc(message) - raise exceptions.ScannerError(description) + 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 elif message.type == gst.MESSAGE_ERROR: - raise exceptions.ScannerError( - encoding.locale_decode(message.parse_error()[0])) + 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 elif message.type == gst.MESSAGE_ASYNC_DONE: From 411bae5a56aaeb5e59e57bdb5939aa76f398511d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 12 Mar 2015 21:58:27 +0100 Subject: [PATCH 14/23] audio: Raise error for unknown protocol types --- mopidy/audio/scan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ed8e9eb9..c4516531 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -92,6 +92,9 @@ class Scanner(object): if not self._src: self._src = gst.element_make_from_uri(gst.URI_SRC, uri) + if not self._src: + raise exceptions.ScannerError('Could not find any elements to ' + 'handle %s URI.' % protocol) utils.setup_proxy(self._src, self._proxy_config) self._pipe.add(self._src) self._src.link(self._typefinder) From 628c8280877e85ba6f027ac3a691a5830e0d2243 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Mar 2015 00:18:50 +0100 Subject: [PATCH 15/23] audio: Recreate scan pipeline for each scan Turns out this code runs a lot faster when we fully destroy the decodebins between scans. And since going to NULL isn't enough I opted to just go for redoing the whole pipeline instead of adding and removing decodebins all the time. As part of this almost all the logic has been ripped out of the scan class and into internal functions. The external interface has been kept the same for now. But we could easily switch to `scan(uri, timeout=1000, proxy=None)` --- mopidy/audio/scan.py | 203 +++++++++++++++++++++---------------------- 1 file changed, 98 insertions(+), 105 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c4516531..50fb8700 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -14,10 +14,13 @@ from mopidy.utils import encoding _missing_plugin_desc = gst.pbutils.missing_plugin_message_get_description -Result = collections.namedtuple( +_Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime')) +_RAW_AUDIO = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') + +# TODO: replace with a scan(uri, timeout=1000, proxy_config=None)? class Scanner(object): """ Helper to get tags and other relevant info from URIs. @@ -31,33 +34,6 @@ class Scanner(object): self._timeout_ms = timeout self._proxy_config = proxy_config or {} - sink = gst.element_factory_make('fakesink') - self._src = None - - def pad_added(src, pad): - return pad.link(sink.get_pad('sink')) - - def have_type(finder, probability, caps): - msg = gst.message_new_application(finder, caps.get_structure(0)) - finder.get_bus().post(msg) - - self._typefinder = gst.element_factory_make('typefind') - self._typefinder.connect('have-type', have_type) - - audio_caps = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') - self._decodebin = gst.element_factory_make('decodebin2') - self._decodebin.set_property('caps', audio_caps) - self._decodebin.connect('pad-added', pad_added) - - self._pipe = gst.element_factory_make('pipeline') - self._pipe.add(self._typefinder) - self._pipe.add(self._decodebin) - self._pipe.add(sink) - - self._typefinder.link(self._decodebin) - - self._bus = self._pipe.get_bus() - def scan(self, uri): """ Scan the given uri collecting relevant metadata. @@ -72,92 +48,109 @@ class Scanner(object): indicating if a seek would succeed. """ tags, duration, seekable, mime = None, None, None, None + pipeline = _setup_pipeline(uri, self._proxy_config) + try: - self._setup(uri) - tags, mime = self._collect() - duration = self._query_duration() - seekable = self._query_seekable() + _start_pipeline(pipeline) + tags, mime = _process(pipeline, self._timeout_ms / 1000.0) + duration = _query_duration(pipeline) + seekable = _query_seekable(pipeline) finally: - self._reset() + pipeline.set_state(gst.STATE_NULL) + del pipeline - return Result(uri, tags, duration, seekable, mime) + return _Result(uri, tags, duration, seekable, mime) - def _setup(self, uri): - """Primes the pipeline for collection.""" - protocol = gst.uri_get_protocol(uri) - if self._src and protocol not in self._src.get_protocols(): - self._src.unlink(self._typefinder) - self._pipe.remove(self._src) - self._src = None - if not self._src: - self._src = gst.element_make_from_uri(gst.URI_SRC, uri) - if not self._src: - raise exceptions.ScannerError('Could not find any elements to ' - 'handle %s URI.' % protocol) - utils.setup_proxy(self._src, self._proxy_config) - self._pipe.add(self._src) - self._src.link(self._typefinder) +# Turns out it's _much_ faster to just create a new pipeline for every as +# decodebins and other elements don't seem to take well to being reused. +def _setup_pipeline(uri, proxy_config=None): + src = gst.element_make_from_uri(gst.URI_SRC, uri) + if not src: + raise exceptions.ScannerError('GStreamer can not open: %s' % uri) - self._pipe.set_state(gst.STATE_READY) - self._src.set_uri(uri) + typefind = gst.element_factory_make('typefind') + decodebin = gst.element_factory_make('decodebin2') + sink = gst.element_factory_make('fakesink') - result = self._pipe.set_state(gst.STATE_PAUSED) - if result == gst.STATE_CHANGE_NO_PREROLL: - # Live sources don't pre-roll, so set to playing to get data. - self._pipe.set_state(gst.STATE_PLAYING) + pipeline = gst.element_factory_make('pipeline') + pipeline.add_many(src, typefind, decodebin, sink) + gst.element_link_many(src, typefind, decodebin) - def _collect(self): - """Polls for messages to collect data.""" - start = time.time() - timeout_s = self._timeout_ms / 1000.0 - tags, mime, missing_description = {}, None, None + if proxy_config: + utils.setup_proxy(src, proxy_config) - while time.time() - start < timeout_s: - if not self._bus.have_pending(): - continue - message = self._bus.pop() + decodebin.set_property('caps', _RAW_AUDIO) + decodebin.connect('pad-added', _pad_added, sink) + typefind.connect('have-type', _have_type, decodebin) - if message.type == gst.MESSAGE_ELEMENT: - if gst.pbutils.is_missing_plugin_message(message): - 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 - 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 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) + + +def _pad_added(element, pad, sink): + return pad.link(sink.get_pad('sink')) + + +def _start_pipeline(pipeline): + if pipeline.set_state(gst.STATE_PAUSED) == gst.STATE_CHANGE_NO_PREROLL: + pipeline.set_state(gst.STATE_PLAYING) + + +def _query_duration(pipeline): + try: + duration = pipeline.query_duration(gst.FORMAT_TIME, None)[0] + except gst.QueryError: + return None + + if duration < 0: + return None + else: + return duration // gst.MSECOND + + +def _query_seekable(pipeline): + query = gst.query_new_seeking(gst.FORMAT_TIME) + pipeline.query(query) + return query.parse_seeking()[1] + + +def _process(pipeline, timeout): + start = time.time() + tags, mime, missing_description = {}, None, None + bus = pipeline.get_bus() + + while time.time() - start < timeout: + if not bus.have_pending(): + continue + message = bus.pop() + + if message.type == gst.MESSAGE_ELEMENT: + if gst.pbutils.is_missing_plugin_message(message): + 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 - elif message.type == gst.MESSAGE_ASYNC_DONE: - if message.src == self._pipe: - return tags, mime - elif message.type == gst.MESSAGE_TAG: - taglist = message.parse_tag() - # Note that this will only keep the last tag. - tags.update(utils.convert_taglist(taglist)) + 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 + elif message.type == gst.MESSAGE_ASYNC_DONE: + if message.src == pipeline: + return tags, mime + elif message.type == gst.MESSAGE_TAG: + taglist = message.parse_tag() + # Note that this will only keep the last tag. + tags.update(utils.convert_taglist(taglist)) - raise exceptions.ScannerError('Timeout after %dms' % self._timeout_ms) - - def _reset(self): - self._pipe.set_state(gst.STATE_NULL) - - def _query_duration(self): - try: - duration = self._pipe.query_duration(gst.FORMAT_TIME, None)[0] - except gst.QueryError: - return None - - if duration < 0: - return None - else: - return duration // gst.MSECOND - - def _query_seekable(self): - query = gst.query_new_seeking(gst.FORMAT_TIME) - self._pipe.query(query) - return query.parse_seeking()[1] + raise exceptions.ScannerError('Timeout after %dms' % (timeout * 1000)) From 73bb6c2a8a18c6e6749878ec4ccd6d36389090e4 Mon Sep 17 00:00:00 2001 From: Thomas Kemmer Date: Fri, 13 Mar 2015 19:10:57 +0100 Subject: [PATCH 16/23] Replace Mopidy-HTTP-Kuechenradio with Mopidy-Mobile. --- docs/ext/mobile.png | Bin 0 -> 88350 bytes docs/ext/web.rst | 14 +++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 docs/ext/mobile.png diff --git a/docs/ext/mobile.png b/docs/ext/mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..983aa27cf394c7de26c0203b80c29cdeb02e7a5c GIT binary patch literal 88350 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
092SqyyM@YNg7?mh?3+EeJHV^*Ro+% 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 literal 0 HcmV?d00001 diff --git a/docs/ext/web.rst b/docs/ext/web.rst index 5abf5b15..425ecf78 100644 --- a/docs/ext/web.rst +++ b/docs/ext/web.rst @@ -30,17 +30,21 @@ To install, run:: pip install Mopidy-API-Explorer -Mopidy-HTTP-Kuechenradio +Mopidy-Mobile ========================= -https://github.com/tkem/mopidy-http-kuechenradio +https://github.com/tkem/mopidy-mobile -A deliberately simple Mopidy Web client for mobile devices. Made with jQuery -Mobile by Thomas Kemmer. +A Mopidy Web client extension and hybrid mobile app, made with Ionic, +AngularJS and Apache Cordova by Thomas Kemmer. + +.. image:: /ext/mobile.png + :width: 1024 + :height: 606 To install, run:: - pip install Mopidy-HTTP-Kuechenradio + pip install Mopidy-Mobile Mopidy-Moped From e4ef6d13caa70f91c51c2cb30462754f117e8ddf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Mar 2015 21:04:37 +0100 Subject: [PATCH 17/23] core: Correct mixer.set_volume() docstring --- mopidy/core/mixer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/core/mixer.py b/mopidy/core/mixer.py index 1f5ada9e..224c09df 100644 --- a/mopidy/core/mixer.py +++ b/mopidy/core/mixer.py @@ -25,10 +25,11 @@ class MixerController(object): def set_volume(self, volume): """Set the volume. - The volume is defined as an integer in range [0..100] or :class:`None` - if the mixer is disabled. + The volume is defined as an integer in range [0..100]. The volume scale is linear. + + Returns :class:`True` if call is successful, otherwise :class:`False`. """ if self._mixer is None: return False From b29f9e10c4ada28a07a9c977e9032d834795aa76 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Mar 2015 21:18:28 +0100 Subject: [PATCH 18/23] core: get_mute() with no mixer returns None ...and not False, because the mute state is unknown (None) and not unmuted (False) when there is no mixer. Note that this change does not affect the MPD responses. --- mopidy/core/mixer.py | 4 +--- tests/core/test_mixer.py | 12 ++++++------ tests/mpd/protocol/test_audio_output.py | 10 ++++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mopidy/core/mixer.py b/mopidy/core/mixer.py index 224c09df..3388d706 100644 --- a/mopidy/core/mixer.py +++ b/mopidy/core/mixer.py @@ -42,9 +42,7 @@ class MixerController(object): :class:`True` if muted, :class:`False` unmuted, :class:`None` if unknown. """ - if self._mixer is None: - return False - else: + if self._mixer is not None: return self._mixer.get_mute().get() def set_mute(self, mute): diff --git a/tests/core/test_mixer.py b/tests/core/test_mixer.py index 6485f3e8..c4126eaa 100644 --- a/tests/core/test_mixer.py +++ b/tests/core/test_mixer.py @@ -42,17 +42,17 @@ class CoreNoneMixerTest(unittest.TestCase): def setUp(self): # noqa: N802 self.core = core.Core(mixer=None, backends=[]) - def test_get_volume_return_none(self): + def test_get_volume_return_none_because_it_is_unknown(self): self.assertEqual(self.core.mixer.get_volume(), None) - def test_set_volume_return_false(self): + def test_set_volume_return_false_because_it_failed(self): self.assertEqual(self.core.mixer.set_volume(30), False) - def test_get_set_mute_return_proper_state(self): - self.assertEqual(self.core.mixer.set_mute(False), False) - self.assertEqual(self.core.mixer.get_mute(), False) + def test_get_mute_return_none_because_it_is_unknown(self): + self.assertEqual(self.core.mixer.get_mute(), None) + + def test_set_mute_return_false_because_it_failed(self): self.assertEqual(self.core.mixer.set_mute(True), False) - self.assertEqual(self.core.mixer.get_mute(), False) @mock.patch.object(mixer.MixerListener, 'send') diff --git a/tests/mpd/protocol/test_audio_output.py b/tests/mpd/protocol/test_audio_output.py index 322bf181..b42b4c56 100644 --- a/tests/mpd/protocol/test_audio_output.py +++ b/tests/mpd/protocol/test_audio_output.py @@ -90,20 +90,22 @@ class AudioOutputHandlerNoneMixerTest(protocol.BaseTestCase): enable_mixer = False def test_enableoutput(self): - self.core.mixer.set_mute(False) + self.assertEqual(self.core.mixer.get_mute().get(), None) self.send_request('enableoutput "0"') self.assertInResponse( 'ACK [52@0] {enableoutput} problems enabling output') - self.assertEqual(self.core.mixer.get_mute().get(), False) + + self.assertEqual(self.core.mixer.get_mute().get(), None) def test_disableoutput(self): - self.core.mixer.set_mute(True) + self.assertEqual(self.core.mixer.get_mute().get(), None) self.send_request('disableoutput "0"') self.assertInResponse( 'ACK [52@0] {disableoutput} problems disabling output') - self.assertEqual(self.core.mixer.get_mute().get(), False) + + self.assertEqual(self.core.mixer.get_mute().get(), None) def test_outputs_when_unmuted(self): self.core.mixer.set_mute(False) From 9adb2c86a9ee2df65c280a0e042d045c6437b38e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Mar 2015 21:20:25 +0100 Subject: [PATCH 19/23] mpd: Make code read better The result of set_mute() and set_volume() is always True or False, never another falsy value like None. --- mopidy/mpd/protocol/audio_output.py | 6 +++--- mopidy/mpd/protocol/playback.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/mpd/protocol/audio_output.py b/mopidy/mpd/protocol/audio_output.py index 6ffedcf1..565ea3d0 100644 --- a/mopidy/mpd/protocol/audio_output.py +++ b/mopidy/mpd/protocol/audio_output.py @@ -14,7 +14,7 @@ def disableoutput(context, outputid): """ if outputid == 0: success = context.core.mixer.set_mute(False).get() - if success is False: + if not success: raise exceptions.MpdSystemError('problems disabling output') else: raise exceptions.MpdNoExistError('No such audio output') @@ -31,7 +31,7 @@ def enableoutput(context, outputid): """ if outputid == 0: success = context.core.mixer.set_mute(True).get() - if success is False: + if not success: raise exceptions.MpdSystemError('problems enabling output') else: raise exceptions.MpdNoExistError('No such audio output') @@ -49,7 +49,7 @@ def toggleoutput(context, outputid): if outputid == 0: mute_status = context.core.mixer.get_mute().get() success = context.core.mixer.set_mute(not mute_status) - if success is False: + if not success: raise exceptions.MpdSystemError('problems toggling output') else: raise exceptions.MpdNoExistError('No such audio output') diff --git a/mopidy/mpd/protocol/playback.py b/mopidy/mpd/protocol/playback.py index 4cf8b2e8..86f2e36b 100644 --- a/mopidy/mpd/protocol/playback.py +++ b/mopidy/mpd/protocol/playback.py @@ -397,7 +397,7 @@ def setvol(context, volume): # NOTE: we use INT as clients can pass in +N etc. value = min(max(0, volume), 100) success = context.core.mixer.set_volume(value).get() - if success is False: + if not success: raise exceptions.MpdSystemError('problems setting volume') From 4ce16ce6385ca02e940ee7dc9293799d35a20061 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Mar 2015 21:31:33 +0100 Subject: [PATCH 20/23] docs: Fix header marker --- docs/ext/web.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ext/web.rst b/docs/ext/web.rst index 425ecf78..b4a9660f 100644 --- a/docs/ext/web.rst +++ b/docs/ext/web.rst @@ -31,7 +31,7 @@ To install, run:: Mopidy-Mobile -========================= +============= https://github.com/tkem/mopidy-mobile From 9e8b3263abf5fa8529dedeb46392f8f19eb723f4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Mar 2015 22:36:35 +0100 Subject: [PATCH 21/23] audio: Use timed pop for message loop and gst clocks --- mopidy/audio/scan.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 50fb8700..cbf4c170 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, division, unicode_literals import collections -import time import pygst pygst.require('0.10') @@ -31,7 +30,7 @@ class Scanner(object): """ def __init__(self, timeout=1000, proxy_config=None): - self._timeout_ms = timeout + self._timeout_ms = int(timeout) self._proxy_config = proxy_config or {} def scan(self, uri): @@ -52,7 +51,7 @@ class Scanner(object): try: _start_pipeline(pipeline) - tags, mime = _process(pipeline, self._timeout_ms / 1000.0) + tags, mime = _process(pipeline, self._timeout_ms) duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: @@ -120,17 +119,19 @@ def _query_seekable(pipeline): return query.parse_seeking()[1] -def _process(pipeline, timeout): - start = time.time() - tags, mime, missing_description = {}, None, None +def _process(pipeline, timeout_ms): + clock = pipeline.get_clock() bus = pipeline.get_bus() + timeout = timeout_ms * gst.MSECOND + tags, mime, missing_description = {}, None, None - while time.time() - start < timeout: - if not bus.have_pending(): - continue - message = bus.pop() + start = clock.get_time() + while timeout > 0: + message = bus.timed_pop(timeout) - if message.type == gst.MESSAGE_ELEMENT: + if message is None: + break + elif message.type == gst.MESSAGE_ELEMENT: if gst.pbutils.is_missing_plugin_message(message): missing_description = encoding.locale_decode( _missing_plugin_desc(message)) @@ -153,4 +154,6 @@ def _process(pipeline, timeout): # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) - raise exceptions.ScannerError('Timeout after %dms' % (timeout * 1000)) + timeout -= clock.get_time() - start + + raise exceptions.ScannerError('Timeout after %dms' % timeout_ms) From faab0b755af9ceb92b2f80b6a9654a670cf38f19 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Mar 2015 22:39:52 +0100 Subject: [PATCH 22/23] audio: Filter for messages we care about, rest will be dropped --- mopidy/audio/scan.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index cbf4c170..3880d91a 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -125,9 +125,12 @@ def _process(pipeline, timeout_ms): timeout = timeout_ms * gst.MSECOND tags, mime, missing_description = {}, None, None + types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR + | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) + start = clock.get_time() while timeout > 0: - message = bus.timed_pop(timeout) + message = bus.timed_pop_filtered(timeout, types) if message is None: break From 6b7f9b4899555c8b2badadc4e2016e0f5ec3ee4d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Mar 2015 22:45:57 +0100 Subject: [PATCH 23/23] docs: Add changelog for the scanner improvements --- docs/changelog.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e3fb9d2..c5808833 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -144,6 +144,13 @@ v0.20.0 (UNRELEASED) - Update scanner to operate with milliseconds for duration. + - Update scanner to use a custom src, typefind and decodebin. This allows us + to catch playlists before we try to decode them. + + - Refactored scanner to create a new pipeline per song, this is needed as + reseting decodebin is much slower than tearing it down and making a fresh + one. + - Add :meth:`mopidy.audio.AudioListener.tags_changed`. Notifies core when new tags are found. @@ -163,6 +170,12 @@ v0.20.0 (UNRELEASED) - Add workaround for volume not persisting across tracks on OS X. (Issue: :issue:`886`, PR: :issue:`958`) +- Improved missing plugin error reporting in scanner. + +- Introduced a new return type for the scanner, a named tuple with ``uri``, + ``tags``, ``duration``, ``seekable`` and ``mime``. Also added support for + checking seekable, and the initial MIME type guess. + **Stream backend** - Add basic tests for the stream library provider.