From 6d856e88bb5cb0b2ac5808da5f6d004fe23d6751 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2016 19:12:59 +0100 Subject: [PATCH 01/39] docs: Add missing packages for Debian stable and Ubuntu < 15.10 Fixes #1434 --- docs/installation/source.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index b5bd7b95..ee2ffad5 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -44,8 +44,10 @@ please follow the directions :ref:`here `. If you use Debian/Ubuntu you can install GStreamer like this:: - sudo apt-get install python-gst-1.0 gstreamer1.0-plugins-good \ - gstreamer1.0-plugins-ugly gstreamer1.0-tools + sudo apt-get install python-gst-1.0 \ + gir1.2-gstreamer-1.0 gir1.2-gst-plugins-base-1.0 \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly \ + gstreamer1.0-tools If you use Arch Linux, install the following packages from the official repository:: From 4691bf5ea6db62ee17c846dce1223879f78f1aff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2016 20:53:14 +0100 Subject: [PATCH 02/39] process: Remove unused BaseThread class --- docs/changelog.rst | 5 +++++ mopidy/internal/process.py | 25 ------------------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9d695afd..4398fecd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -129,6 +129,11 @@ Cleanups - Catch errors when loading :confval:`logging/config_file`. (Fixes: :issue:`1320`) +- **Breaking:** Removed unused internal + :class:`mopidy.internal.process.BaseThread`. This breaks Mopidy-Spotify + 1.4.0. Versions < 1.4.0 was already broken by Mopidy 1.1, while versions >= + 2.0 doesn't use this class. + Audio ----- diff --git a/mopidy/internal/process.py b/mopidy/internal/process.py index 0710a82f..8c8af18f 100644 --- a/mopidy/internal/process.py +++ b/mopidy/internal/process.py @@ -49,28 +49,3 @@ def stop_remaining_actors(): pykka.ActorRegistry.stop_all() num_actors = len(pykka.ActorRegistry.get_all()) logger.debug('All actors stopped.') - - -class BaseThread(threading.Thread): - - def __init__(self): - super(BaseThread, self).__init__() - # No thread should block process from exiting - self.daemon = True - - def run(self): - logger.debug('%s: Starting thread', self.name) - try: - self.run_inside_try() - except KeyboardInterrupt: - logger.info('Interrupted by user') - except ImportError as e: - logger.error(e) - except pykka.ActorDeadError as e: - logger.warning(e) - except Exception as e: - logger.exception(e) - logger.debug('%s: Exiting thread', self.name) - - def run_inside_try(self): - raise NotImplementedError From 3a7e7cdde04a032fec3d6986955f2d12dd8d3aca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2016 21:30:59 +0100 Subject: [PATCH 03/39] process: Rename exit_handler() to sigterm_handler() --- mopidy/__main__.py | 2 +- mopidy/internal/process.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index ee87b82d..86a0c19c 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -27,7 +27,7 @@ def main(): log.bootstrap_delayed_logging() logger.info('Starting Mopidy %s', versioning.get_version()) - signal.signal(signal.SIGTERM, process.exit_handler) + signal.signal(signal.SIGTERM, process.sigterm_handler) # Windows does not have signal.SIGUSR1 if hasattr(signal, 'SIGUSR1'): signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) diff --git a/mopidy/internal/process.py b/mopidy/internal/process.py index 8c8af18f..4bf681dd 100644 --- a/mopidy/internal/process.py +++ b/mopidy/internal/process.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, unicode_literals import logging -import signal import threading import pykka @@ -12,20 +11,23 @@ from mopidy.compat import thread logger = logging.getLogger(__name__) -SIGNALS = dict( - (k, v) for v, k in signal.__dict__.items() - if v.startswith('SIG') and not v.startswith('SIG_')) - - def exit_process(): logger.debug('Interrupting main...') thread.interrupt_main() logger.debug('Interrupted main') -def exit_handler(signum, frame): - """A :mod:`signal` handler which will exit the program on signal.""" - logger.info('Got %s signal', SIGNALS[signum]) +def sigterm_handler(signum, frame): + """A :mod:`signal` handler which will exit the program on signal. + + This function is not called when the process' main thread is running a GLib + mainloop. In that case, the GLib mainloop must listen for SIGTERM signals + and quit itself. + + For Mopidy subcommands that does not run the GLib mainloop, this handler + ensures a proper shutdown of the process on SIGTERM. + """ + logger.info('Got SIGTERM signal. Exiting...') exit_process() From e88b2a7beb24f33e4821f75368ef1727d1d384bc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2016 21:37:25 +0100 Subject: [PATCH 04/39] commands: Make GLib quit mainloop on SIGTERM Fixes #1435 --- mopidy/commands.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 74905f8f..50590172 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -5,6 +5,7 @@ import collections import contextlib import logging import os +import signal import sys import pykka @@ -13,7 +14,7 @@ from mopidy import config as config_lib, exceptions from mopidy.audio import Audio from mopidy.core import Core from mopidy.internal import deps, process, timer, versioning -from mopidy.internal.gi import GLib, GObject +from mopidy.internal.gi import GLib logger = logging.getLogger(__name__) @@ -283,7 +284,13 @@ class RootCommand(Command): help='`section/key=value` values to override config options') def run(self, args, config): - loop = GObject.MainLoop() + def on_sigterm(loop): + logger.info('GLib mainloop got SIGTERM. Exiting...') + loop.quit() + + loop = GLib.MainLoop() + GLib.unix_signal_add( + GLib.PRIORITY_DEFAULT, signal.SIGTERM, on_sigterm, loop) mixer_class = self.get_mixer_class(config, args.registry['mixer']) backend_classes = args.registry['backend'] @@ -300,6 +307,7 @@ class RootCommand(Command): backends = self.start_backends(config, backend_classes, audio) core = self.start_core(config, mixer, backends, audio) self.start_frontends(config, frontend_classes, core) + logger.info('Starting GLib mainloop') loop.run() except (exceptions.BackendError, exceptions.FrontendError, From 68add6cda967ebac3bb40a0809201ce661a1858e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 10 Feb 2016 23:02:14 +0100 Subject: [PATCH 05/39] audio: Workaround crash caused by race Fixes #1430. See #1222 for explanation and proper fix. --- docs/changelog.rst | 4 ++++ mopidy/audio/actor.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4398fecd..ea1b5a76 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -165,6 +165,10 @@ Audio should prevent seeking in Mopidy-Spotify from glitching. (Fixes: :issue:`1404`) +- Workaround crash caused by a race that does not seem to affect functionality. + This should be fixed properly together with :issue:`1222`. (Fixes: + :issue:`1430`, PR: :issue:`1438`) + Gapless ------- diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 501a9d45..02ad48ed 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -257,7 +257,11 @@ class _Handler(object): new_state = _GST_STATE_MAPPING[new_state] old_state, self._audio.state = self._audio.state, new_state - target_state = _GST_STATE_MAPPING[self._audio._target_state] + target_state = _GST_STATE_MAPPING.get(self._audio._target_state) + if target_state is None: + # XXX: Workaround for #1430, to be fixed properly by #1222. + logger.debug('Race condition happened. See #1222 and #1430.') + return if target_state == new_state: target_state = None From 0580a4668898ecb7aeb34bbfcf780634114380db Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Sat, 13 Feb 2016 23:37:22 +0100 Subject: [PATCH 06/39] audio: Add a config option for queue buffer size It may help to increase this for users that are experiencing buffering before track changes. Workaround for #1409. --- docs/changelog.rst | 4 ++++ docs/config.rst | 10 ++++++++++ mopidy/audio/actor.py | 5 +++++ mopidy/config/__init__.py | 1 + mopidy/config/default.conf | 1 + 5 files changed, 21 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index ea1b5a76..6cdf5365 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -169,6 +169,10 @@ Audio This should be fixed properly together with :issue:`1222`. (Fixes: :issue:`1430`, PR: :issue:`1438`) +- Add a new config option, buffer_time, for setting the buffer time of the + GStreamer queue. If you experience buffering before track changes, it may + help to increase this. Workaround for :issue:`1409`. + Gapless ------- diff --git a/docs/config.rst b/docs/config.rst index efbf5e86..bf131dbe 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -155,6 +155,16 @@ These are the available audio configurations. For specific use cases, see ``gst-inspect-1.0`` to see what output properties can be set on the sink. For example: ``gst-inspect-1.0 shout2send`` +.. confval:: audio/buffer_time + + Buffer size in milliseconds. + + Expects an integer above 0. + + Sets the buffer size of the GStreamer queue. If you experience buffering + before track changes, it may help to increase this. The default is letting + GStreamer decide the size. + Logging configuration ===================== diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 02ad48ed..f825a768 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -470,6 +470,11 @@ class Audio(pykka.ThreadingActor): # systems. So leave the default to play it safe. queue = Gst.ElementFactory.make('queue') + if self._config['audio']['buffer_time'] > 0: + queue.set_property( + 'max-size-time', + self._config['audio']['buffer_time'] * Gst.MSECOND) + audio_sink.add(queue) audio_sink.add(self._outputs) diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 042c20d9..21a6a00b 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -38,6 +38,7 @@ _audio_schema['mixer_track'] = Deprecated() _audio_schema['mixer_volume'] = Integer(optional=True, minimum=0, maximum=100) _audio_schema['output'] = String() _audio_schema['visualizer'] = Deprecated() +_audio_schema['buffer_time'] = Integer(optional=True, minimum=1) _proxy_schema = ConfigSchema('proxy') _proxy_schema['scheme'] = String(optional=True, diff --git a/mopidy/config/default.conf b/mopidy/config/default.conf index 675381d9..c747703b 100644 --- a/mopidy/config/default.conf +++ b/mopidy/config/default.conf @@ -15,6 +15,7 @@ config_file = mixer = software mixer_volume = output = autoaudiosink +buffer_time = [proxy] scheme = From 3e781310f998100eb34d4dcd767d7337d98e67f6 Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Sun, 14 Feb 2016 00:15:27 +0100 Subject: [PATCH 07/39] tests: Add buffer_time to test config --- tests/audio/test_actor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 2bcc792a..b6ec6170 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -22,6 +22,7 @@ from tests import dummy_audio, path_to_data_dir class BaseTest(unittest.TestCase): config = { 'audio': { + 'buffer_time': None, 'mixer': 'fakemixer track_max_volume=65536', 'mixer_track': None, 'mixer_volume': None, @@ -38,6 +39,7 @@ class BaseTest(unittest.TestCase): def setUp(self): # noqa: N802 config = { 'audio': { + 'buffer_time': None, 'mixer': 'foomixer', 'mixer_volume': None, 'output': 'testoutput', From 59dadc653594941179b86971c797145b1b22cdaf Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Sun, 14 Feb 2016 00:21:22 +0100 Subject: [PATCH 08/39] docs: Link to config and clarify buffer size --- docs/changelog.rst | 6 +++--- docs/config.rst | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6cdf5365..931e1437 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -169,9 +169,9 @@ Audio This should be fixed properly together with :issue:`1222`. (Fixes: :issue:`1430`, PR: :issue:`1438`) -- Add a new config option, buffer_time, for setting the buffer time of the - GStreamer queue. If you experience buffering before track changes, it may - help to increase this. Workaround for :issue:`1409`. +- Add a new config option, :confval:`audio/buffer_time`, for setting the buffer + time of the GStreamer queue. If you experience buffering before track + changes, it may help to increase this. Workaround for :issue:`1409`. Gapless ------- diff --git a/docs/config.rst b/docs/config.rst index bf131dbe..b0d2e52e 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -162,8 +162,9 @@ These are the available audio configurations. For specific use cases, see Expects an integer above 0. Sets the buffer size of the GStreamer queue. If you experience buffering - before track changes, it may help to increase this. The default is letting - GStreamer decide the size. + before track changes, it may help to increase this, possibly by at least a + few seconds. The default is letting GStreamer decide the size, which at the + time of this writing is 1000. Logging configuration From 6aef96a0d3a6d609abeb7dd7f3a7b17cfe3a6f04 Mon Sep 17 00:00:00 2001 From: Thomas Kemmer Date: Sun, 14 Feb 2016 12:07:22 +0100 Subject: [PATCH 09/39] Fix #1428: Add m3u/base_dir confval. --- docs/changelog.rst | 3 +++ docs/ext/m3u.rst | 6 ++++++ mopidy/m3u/__init__.py | 1 + mopidy/m3u/ext.conf | 1 + mopidy/m3u/playlists.py | 5 +++-- tests/m3u/test_playlists.py | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 931e1437..32453c13 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -55,6 +55,9 @@ Local backend M3U backend ----------- +- Add :confval:`m3u/base_dir` for resolving relative paths in M3U + files. (Fixes: :issue:`1428`, PR: :issue:`1442`) + - Derive track name from file name for non-extended M3U playlists. (Fixes: :issue:`1364`, PR: :issue:`1369`) diff --git a/docs/ext/m3u.rst b/docs/ext/m3u.rst index 37dc60be..35bd2036 100644 --- a/docs/ext/m3u.rst +++ b/docs/ext/m3u.rst @@ -55,6 +55,12 @@ See :ref:`config` for general help on configuring Mopidy. Path to directory with M3U files. Unset by default, in which case the extension's data dir is used to store playlists. +.. confval:: m3u/base_dir + + Path to base directory for resolving relative paths in M3U files. + If not set, relative paths are resolved based on the M3U file's + location. + .. confval:: m3u/default_encoding Text encoding used for files with extension ``.m3u``. Default is diff --git a/mopidy/m3u/__init__.py b/mopidy/m3u/__init__.py index df769c88..6a7fad9a 100644 --- a/mopidy/m3u/__init__.py +++ b/mopidy/m3u/__init__.py @@ -21,6 +21,7 @@ class Extension(ext.Extension): def get_config_schema(self): schema = super(Extension, self).get_config_schema() + schema['base_dir'] = config.Path(optional=True) schema['default_encoding'] = config.String() schema['default_extension'] = config.String(choices=['.m3u', '.m3u8']) schema['playlists_dir'] = config.Path(optional=True) diff --git a/mopidy/m3u/ext.conf b/mopidy/m3u/ext.conf index 862bc6f7..16291c83 100644 --- a/mopidy/m3u/ext.conf +++ b/mopidy/m3u/ext.conf @@ -1,5 +1,6 @@ [m3u] enabled = true playlists_dir = +base_dir = $XDG_MUSIC_DIR default_encoding = latin-1 default_extension = .m3u8 diff --git a/mopidy/m3u/playlists.py b/mopidy/m3u/playlists.py index 7e4e39ff..28be28d9 100644 --- a/mopidy/m3u/playlists.py +++ b/mopidy/m3u/playlists.py @@ -60,6 +60,7 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider): self._playlists_dir = Extension.get_data_dir(config) else: self._playlists_dir = ext_config['playlists_dir'] + self._base_dir = ext_config['base_dir'] or self._playlists_dir self._default_encoding = ext_config['default_encoding'] self._default_extension = ext_config['default_extension'] @@ -97,7 +98,7 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider): path = translator.uri_to_path(uri) try: with self._open(path, 'r') as fp: - items = translator.load_items(fp, self._playlists_dir) + items = translator.load_items(fp, self._base_dir) except EnvironmentError as e: log_environment_error('Error reading playlist %s' % uri, e) else: @@ -107,7 +108,7 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider): path = translator.uri_to_path(uri) try: with self._open(path, 'r') as fp: - items = translator.load_items(fp, self._playlists_dir) + items = translator.load_items(fp, self._base_dir) mtime = os.path.getmtime(self._abspath(path)) except EnvironmentError as e: log_environment_error('Error reading playlist %s' % uri, e) diff --git a/tests/m3u/test_playlists.py b/tests/m3u/test_playlists.py index 664da9e9..e0ea1ce4 100644 --- a/tests/m3u/test_playlists.py +++ b/tests/m3u/test_playlists.py @@ -24,6 +24,7 @@ class M3UPlaylistsProviderTest(unittest.TestCase): config = { 'm3u': { 'enabled': True, + 'base_dir': None, 'default_encoding': 'latin-1', 'default_extension': '.m3u', 'playlists_dir': path_to_data_dir(''), @@ -33,6 +34,7 @@ class M3UPlaylistsProviderTest(unittest.TestCase): def setUp(self): # noqa: N802 self.config['m3u']['playlists_dir'] = tempfile.mkdtemp() self.playlists_dir = self.config['m3u']['playlists_dir'] + self.base_dir = self.config['m3u']['base_dir'] or self.playlists_dir audio = dummy_audio.create_proxy() backend = M3UBackend.start( @@ -261,6 +263,32 @@ class M3UPlaylistsProviderTest(unittest.TestCase): self.assertEqual(playlist.name, result.name) self.assertEqual(track.uri, result.tracks[0].uri) + def test_playlist_with_absolute_path(self): + track = Track(uri='/tmp/test.mp3') + filepath = b'/tmp/test.mp3' + playlist = self.core.playlists.create('test') + playlist = playlist.replace(tracks=[track]) + playlist = self.core.playlists.save(playlist) + + self.assertEqual(len(self.core.playlists.as_list()), 1) + result = self.core.playlists.lookup('m3u:test.m3u') + self.assertEqual('m3u:test.m3u', result.uri) + self.assertEqual(playlist.name, result.name) + self.assertEqual('file://' + filepath, result.tracks[0].uri) + + def test_playlist_with_relative_path(self): + track = Track(uri='test.mp3') + filepath = os.path.join(self.base_dir, b'test.mp3') + playlist = self.core.playlists.create('test') + playlist = playlist.replace(tracks=[track]) + playlist = self.core.playlists.save(playlist) + + self.assertEqual(len(self.core.playlists.as_list()), 1) + result = self.core.playlists.lookup('m3u:test.m3u') + self.assertEqual('m3u:test.m3u', result.uri) + self.assertEqual(playlist.name, result.name) + self.assertEqual('file://' + filepath, result.tracks[0].uri) + def test_playlist_sort_order(self): def check_order(playlists, names): self.assertEqual(names, [playlist.name for playlist in playlists]) @@ -303,6 +331,13 @@ class M3UPlaylistsProviderTest(unittest.TestCase): self.assertIsNone(item_refs) +class M3UPlaylistsProviderBaseDirectoryTest(M3UPlaylistsProviderTest): + + def setUp(self): # noqa: N802 + self.config['m3u']['base_dir'] = tempfile.mkdtemp() + super(M3UPlaylistsProviderBaseDirectoryTest, self).setUp() + + class DeprecatedM3UPlaylistsProviderTest(M3UPlaylistsProviderTest): def run(self, result=None): From c23cad5d134df467aa80b997250f18a58fea2ec3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 13 Feb 2016 22:34:09 +0100 Subject: [PATCH 10/39] audio: Only emit tags changed when tags changed. Previously we alerted AudioListeners about all new tags, now we filter it down to just the changed ones. Only real reason for this is that the changed messages spam the log output making debugging harder. --- mopidy/audio/actor.py | 16 +++++++++++++--- mopidy/audio/tags.py | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index f825a768..ea4a6ed9 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -326,9 +326,19 @@ class _Handler(object): def on_tag(self, taglist): tags = tags_lib.convert_taglist(taglist) gst_logger.debug('Got TAG bus message: tags=%r', dict(tags)) - self._audio._tags.update(tags) - logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) - AudioListener.send('tags_changed', tags=tags.keys()) + + # TODO: Add proper tests for only emitting changed tags. + unique = object() + changed = [] + for key, value in tags.items(): + # Update any tags that changed, and store changed keys. + if self._audio._tags.get(key, unique) != value: + self._audio._tags[key] = value + changed.append(key) + + if changed: + logger.debug('Audio event: tags_changed(tags=%r)', changed) + AudioListener.send('tags_changed', tags=changed) def on_missing_plugin(self, msg): desc = GstPbutils.missing_plugin_message_get_description(msg) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 62784bc0..38a0bac9 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -58,6 +58,7 @@ gstreamer-GstTagList.html log.TRACE_LOG_LEVEL, 'Ignoring unknown tag data: %r = %r', tag, value) + # TODO: dict(result) to not leak the defaultdict, or just use setdefault? return result From b63b3c288add2826b7cb44abefe378ffb4cfc668 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 13 Feb 2016 22:44:31 +0100 Subject: [PATCH 11/39] audio: Postpone tags until after stream-start When a new URI gets set we create a pending tags dictionary. This gets all the tags until stream-start, at which point they are all emitted at once. During track playback tags works as before. This ensure we don't prematurely tell clients about metadata changes. --- mopidy/audio/actor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ea4a6ed9..64300ff9 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -327,6 +327,11 @@ class _Handler(object): tags = tags_lib.convert_taglist(taglist) gst_logger.debug('Got TAG bus message: tags=%r', dict(tags)) + # Postpone emitting tags until stream start. + if self._audio._pending_tags is not None: + self._audio._pending_tags.update(tags) + return + # TODO: Add proper tests for only emitting changed tags. unique = object() changed = [] @@ -359,6 +364,14 @@ class _Handler(object): logger.debug('Audio event: stream_changed(uri=%r)', uri) AudioListener.send('stream_changed', uri=uri) + # Emit any postponed tags that we got after about-to-finish. + tags, self._audio._pending_tags = self._audio._pending_tags, None + self._audio._tags = tags + + if tags: + logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) + AudioListener.send('tags_changed', tags=tags.keys()) + def on_segment(self, segment): gst_logger.debug( 'Got SEGMENT pad event: ' @@ -396,6 +409,7 @@ class Audio(pykka.ThreadingActor): self._buffering = False self._tags = {} self._pending_uri = None + self._pending_tags = None self._playbin = None self._outputs = None @@ -546,8 +560,8 @@ class Audio(pykka.ThreadingActor): else: current_volume = None - self._tags = {} # TODO: add test for this somehow self._pending_uri = uri + self._pending_tags = {} self._playbin.set_property('uri', uri) if self.mixer is not None and current_volume is not None: From d20621c801f56eaac4eb675879ba0cecab82f7e2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 14 Feb 2016 12:35:16 +0100 Subject: [PATCH 12/39] docs: Add changelog entry for tags_changed --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 32453c13..938e19d6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -176,6 +176,10 @@ Audio time of the GStreamer queue. If you experience buffering before track changes, it may help to increase this. Workaround for :issue:`1409`. +- ``tags_changed`` events are only emitted for fields that have changed. + Previous behavior was to emit this for all fields received from GStreamer. + (PR: :issue:`1439`) + Gapless ------- From 3a8d896146f6bc443b76f8d9689cb7510fc304e0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 12:49:15 +0100 Subject: [PATCH 13/39] core: Add TODO for testing unplayable-by-backend tracks --- tests/core/test_playback.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 4f20830e..f936ad9d 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -24,6 +24,11 @@ class TestBackend(pykka.ThreadingActor, backend.Backend): super(TestBackend, self).__init__() self.playback = backend.PlaybackProvider(audio=audio, backend=self) + def translate_uri(self, uri): + if 'unplayable' in uri: + return None + return uri + class BaseTest(unittest.TestCase): config = {'core': {'max_tracklist_length': 10000}} @@ -654,6 +659,11 @@ class TestUnplayableURI(BaseTest): self.assertFalse(success) +class TestUnplayableByBackend(BaseTest): + + pass # TODO + + class SeekTest(BaseTest): def test_seek_normalizes_negative_positions_to_zero(self): From 0539e4e8fee31a90659d0b989eb8fd70b354e7ea Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 15:47:18 +0100 Subject: [PATCH 14/39] Revert "core: Add TODO for testing unplayable-by-backend tracks" This reverts commit 3a8d896146f6bc443b76f8d9689cb7510fc304e0. --- tests/core/test_playback.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index f936ad9d..4f20830e 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -24,11 +24,6 @@ class TestBackend(pykka.ThreadingActor, backend.Backend): super(TestBackend, self).__init__() self.playback = backend.PlaybackProvider(audio=audio, backend=self) - def translate_uri(self, uri): - if 'unplayable' in uri: - return None - return uri - class BaseTest(unittest.TestCase): config = {'core': {'max_tracklist_length': 10000}} @@ -659,11 +654,6 @@ class TestUnplayableURI(BaseTest): self.assertFalse(success) -class TestUnplayableByBackend(BaseTest): - - pass # TODO - - class SeekTest(BaseTest): def test_seek_normalizes_negative_positions_to_zero(self): From cc82e68a5804f404ba76f370643221ec45cf212e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 15:22:25 +0100 Subject: [PATCH 15/39] core: Remove unplayable track in consume mode Fixes #1418 This was previously fixed in 1.1.2, but the fix was skipped in when release-1.1 was merged into develop in #1400, thus no changelog entry. --- mopidy/core/playback.py | 1 - mopidy/core/tracklist.py | 2 ++ tests/core/test_playback.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 63259f7d..e0eab403 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -346,7 +346,6 @@ class PlaybackController(object): pending = tl_track or current or self.core.tracklist.next_track(None) while pending: - # TODO: should we consume unplayable tracks in this loop? if self._change(pending, PlaybackState.PLAYING): break else: diff --git a/mopidy/core/tracklist.py b/mopidy/core/tracklist.py index 2a4ec8b6..6d7ceeb7 100644 --- a/mopidy/core/tracklist.py +++ b/mopidy/core/tracklist.py @@ -621,6 +621,8 @@ class TracklistController(object): def _mark_unplayable(self, tl_track): """Internal method for :class:`mopidy.core.PlaybackController`.""" logger.warning('Track is not playable: %s', tl_track.track.uri) + if self.get_consume() and tl_track is not None: + self.remove({'tlid': [tl_track.tlid]}) if self.get_random() and tl_track in self._shuffled: self._shuffled.remove(tl_track) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 4f20830e..1def8431 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -256,6 +256,20 @@ class TestConsumeHandling(BaseTest): self.assertNotIn(tl_track, self.core.tracklist.get_tl_tracks()) + def test_next_in_consume_mode_removes_unplayable_track(self): + last_playable_tl_track = self.core.tracklist.get_tl_tracks()[-2] + unplayable_tl_track = self.core.tracklist.get_tl_tracks()[-1] + self.audio.trigger_fake_playback_failure(unplayable_tl_track.track.uri) + + self.core.playback.play(last_playable_tl_track) + self.core.tracklist.set_consume(True) + + self.core.playback.next() + self.replay_events() + + self.assertNotIn( + unplayable_tl_track, self.core.tracklist.get_tl_tracks()) + def test_on_about_to_finish_in_consume_mode_removes_finished_track(self): tl_track = self.core.tracklist.get_tl_tracks()[0] From a42ce9f00ecee1ab249e577856e9f0dd169ebc9e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 15:45:13 +0100 Subject: [PATCH 16/39] core: Test next/prev skips over unplayable tracks Fixes #1418 Based on tests that was present in 1.1.2 but dropped in the #1400 merge. --- tests/core/test_playback.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 1def8431..7651b9ef 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -185,6 +185,17 @@ class TestNextHandling(BaseTest): self.assertIn(tl_track, self.core.tracklist.tl_tracks) + def test_next_skips_over_unplayable_track(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + self.audio.trigger_fake_playback_failure(tl_tracks[1].track.uri) + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.core.playback.next() + self.replay_events() + + assert self.core.playback.get_current_tl_track() == tl_tracks[2] + class TestPreviousHandling(BaseTest): # TODO Test previous() more @@ -230,6 +241,17 @@ class TestPreviousHandling(BaseTest): self.assertIn(tl_tracks[1], self.core.tracklist.tl_tracks) + def test_previous_skips_over_unplayable_track(self): + tl_tracks = self.core.tracklist.get_tl_tracks() + self.audio.trigger_fake_playback_failure(tl_tracks[1].track.uri) + self.core.playback.play(tl_tracks[2]) + self.replay_events() + + self.core.playback.previous() + self.replay_events() + + assert self.core.playback.get_current_tl_track() == tl_tracks[0] + class OnAboutToFinishTest(BaseTest): From 9b18ff07ee58a0ea91f45d40b0918b4da491177e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 16:12:43 +0100 Subject: [PATCH 17/39] core: Readd regression test for #1352 Fixes #1418 Based on test that was present in 1.1.2 but dropped in the #1400 merge. --- tests/core/test_playback.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 7651b9ef..a43724f0 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -1000,3 +1000,30 @@ class TestBug1177Regression(unittest.TestCase): c.playback.pause() c.playback.next() b.playback.change_track.assert_called_once_with(track2) + + +class TestBug1352Regression(BaseTest): + tracks = [ + Track(uri='dummy:a', length=40000), + Track(uri='dummy:b', length=40000), + ] + + def test_next_when_paused_updates_history(self): + self.core.history._add_track = mock.Mock() + self.core.tracklist._mark_playing = mock.Mock() + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.playback.play() + self.replay_events() + + self.core.history._add_track.assert_called_once_with(self.tracks[0]) + self.core.tracklist._mark_playing.assert_called_once_with(tl_tracks[0]) + self.core.history._add_track.reset_mock() + self.core.tracklist._mark_playing.reset_mock() + + self.playback.pause() + self.playback.next() + self.replay_events() + + self.core.history._add_track.assert_called_once_with(self.tracks[1]) + self.core.tracklist._mark_playing.assert_called_once_with(tl_tracks[1]) From b293a116b6055bbb28b1df1b6fd936169a67aac8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 13 Feb 2016 23:16:42 +0100 Subject: [PATCH 18/39] audio: Make sure about to finish skips unplayable tracks --- mopidy/core/playback.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index e0eab403..76e80afd 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -250,17 +250,16 @@ class PlaybackController(object): if self._state == PlaybackState.STOPPED: return - # TODO: check that we always have a current track - original_tl_track = self.get_current_tl_track() - next_tl_track = self.core.tracklist.eot_track(original_tl_track) - - # TODO: only set pending if we have a backend that can play it? - # TODO: skip tracks that don't have a backend? - self._pending_tl_track = next_tl_track - backend = self._get_backend(next_tl_track) - - if backend: - backend.playback.change_track(next_tl_track.track).get() + pending = self.core.tracklist.eot_track(self._current_tl_track) + while pending: + # TODO: Avoid infinite loops if all tracks are unplayable. + backend = self._get_backend(pending) + if backend and backend.playback.change_track(pending.track).get(): + self._pending_tl_track = pending + break + else: + self.core.tracklist._mark_unplayable(pending) + pending = self.core.tracklist.eot_track(pending) def _on_tracklist_change(self): """ From 08ebb3b699044bd95f88a2aa22e3dd9fb9ef4cca Mon Sep 17 00:00:00 2001 From: Trygve Aaberge Date: Sun, 14 Feb 2016 16:46:08 +0100 Subject: [PATCH 19/39] docs: Library view is only slow with ncmpcpp <= 0.5 --- docs/clients/mpd.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index ee1b1903..e1cb6019 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -30,14 +30,17 @@ supported)" mode because the client tries to fetch all known metadata and do the search on the client side. The two other search modes works nicely, so this is not a problem. -The library view is very slow when used together with Mopidy-Spotify. A -workaround is to edit the ncmpcpp configuration file +With ncmpcpp <= 0.5, the library view is very slow when used together with +Mopidy-Spotify. A workaround is to edit the ncmpcpp configuration file (:file:`~/.ncmpcpp/config`) and set:: media_library_display_date = "no" With this change ncmpcpp's library view will still be a bit slow, but usable. +Note that this option was removed in ncmpcpp 0.6, but with this version, the +library view works well without it. + ncmpc ----- From 76ab5ffb041a6b26dc9fa890976f25fc8fcef2d5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 14 Feb 2016 17:16:31 +0100 Subject: [PATCH 20/39] core: Make sure exceptions from backend's change_track is handled Also adds TODOs for the rest of the backend calls in playback which all need to assume backends can and will screw up. --- mopidy/core/playback.py | 18 ++++++++- tests/core/test_playback.py | 74 ++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 76e80afd..3597f920 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -135,6 +135,7 @@ class PlaybackController(object): return self._pending_position backend = self._get_backend(self.get_current_tl_track()) if backend: + # TODO: Wrap backend call in error handling. return backend.playback.get_time_position().get() else: return 0 @@ -253,6 +254,7 @@ class PlaybackController(object): pending = self.core.tracklist.eot_track(self._current_tl_track) while pending: # TODO: Avoid infinite loops if all tracks are unplayable. + # TODO: Wrap backend call in error handling. backend = self._get_backend(pending) if backend and backend.playback.change_track(pending.track).get(): self._pending_tl_track = pending @@ -299,6 +301,7 @@ class PlaybackController(object): def pause(self): """Pause playback.""" backend = self._get_backend(self.get_current_tl_track()) + # TODO: Wrap backend call in error handling. if not backend or backend.playback.pause().get(): # TODO: switch to: # backend.track(pause) @@ -366,10 +369,18 @@ class PlaybackController(object): if not backend: return False + # TODO: Wrap backend call in error handling. backend.playback.prepare_change() - if not backend.playback.change_track(pending_tl_track.track).get(): - return False # TODO: test for this path + try: + if not backend.playback.change_track(pending_tl_track.track).get(): + return False + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + return False + + # TODO: Wrap backend calls in error handling. if state == PlaybackState.PLAYING: try: return backend.playback.play().get() @@ -418,6 +429,7 @@ class PlaybackController(object): if self.get_state() != PlaybackState.PAUSED: return backend = self._get_backend(self.get_current_tl_track()) + # TODO: Wrap backend call in error handling. if backend and backend.playback.resume().get(): self.set_state(PlaybackState.PLAYING) # TODO: trigger via gst messages @@ -475,6 +487,7 @@ class PlaybackController(object): backend = self._get_backend(self.get_current_tl_track()) if not backend: return False + # TODO: Wrap backend call in error handling. return backend.playback.seek(time_position).get() def stop(self): @@ -482,6 +495,7 @@ class PlaybackController(object): if self.get_state() != PlaybackState.STOPPED: self._last_position = self.get_time_position() backend = self._get_backend(self.get_current_tl_track()) + # TODO: Wrap backend call in error handling. if not backend or backend.playback.stop().get(): self.set_state(PlaybackState.STOPPED) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index a43724f0..860ce556 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -13,6 +13,16 @@ from mopidy.models import Track from tests import dummy_audio +class TestPlaybackProvider(backend.PlaybackProvider): + def translate_uri(self, uri): + if 'error' in uri: + raise Exception(uri) + elif 'unplayable' in uri: + return None + else: + return uri + + # TODO: Replace this with dummy_backend now that it uses a real # playbackprovider Since we rely on our DummyAudio to actually emit events we # need a "real" backend and not a mock so the right calls make it through to @@ -22,7 +32,7 @@ class TestBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): super(TestBackend, self).__init__() - self.playback = backend.PlaybackProvider(audio=audio, backend=self) + self.playback = TestPlaybackProvider(audio=audio, backend=self) class BaseTest(unittest.TestCase): @@ -196,6 +206,36 @@ class TestNextHandling(BaseTest): assert self.core.playback.get_current_tl_track() == tl_tracks[2] + def test_next_skips_over_change_track_error(self): + # Trigger an exception in translate_uri. + track = Track(uri='dummy:error', length=1234) + self.core.tracklist.add(tracks=[track], at_position=1) + + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play() + self.replay_events() + + self.core.playback.next() + self.replay_events() + + assert self.core.playback.get_current_tl_track() == tl_tracks[2] + + def test_next_skips_over_change_track_unplayable(self): + # Make translate_uri return None. + track = Track(uri='dummy:unplayable', length=1234) + self.core.tracklist.add(tracks=[track], at_position=1) + + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play() + self.replay_events() + + self.core.playback.next() + self.replay_events() + + assert self.core.playback.get_current_tl_track() == tl_tracks[2] + class TestPreviousHandling(BaseTest): # TODO Test previous() more @@ -252,8 +292,38 @@ class TestPreviousHandling(BaseTest): assert self.core.playback.get_current_tl_track() == tl_tracks[0] + def test_previous_skips_over_change_track_error(self): + # Trigger an exception in translate_uri. + track = Track(uri='dummy:error', length=1234) + self.core.tracklist.add(tracks=[track], at_position=1) -class OnAboutToFinishTest(BaseTest): + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[2]) + self.replay_events() + + self.core.playback.previous() + self.replay_events() + + assert self.core.playback.get_current_tl_track() == tl_tracks[0] + + def test_previous_skips_over_change_track_unplayable(self): + # Makes translate_uri return None. + track = Track(uri='dummy:unplayable', length=1234) + self.core.tracklist.add(tracks=[track], at_position=1) + + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[2]) + self.replay_events() + + self.core.playback.previous() + self.replay_events() + + assert self.core.playback.get_current_tl_track() == tl_tracks[0] + + +class TestOnAboutToFinish(BaseTest): def test_on_about_to_finish_keeps_finished_track_in_tracklist(self): tl_track = self.core.tracklist.get_tl_tracks()[0] From 79a4835e4eb43024153fe2eb7185791e24a591a2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 14 Feb 2016 17:23:20 +0100 Subject: [PATCH 21/39] core: Add tests for change_track failing in about-to-finish --- mopidy/core/playback.py | 18 ++++++++++++------ tests/core/test_playback.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 3597f920..d6c470f2 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -254,13 +254,19 @@ class PlaybackController(object): pending = self.core.tracklist.eot_track(self._current_tl_track) while pending: # TODO: Avoid infinite loops if all tracks are unplayable. - # TODO: Wrap backend call in error handling. backend = self._get_backend(pending) - if backend and backend.playback.change_track(pending.track).get(): - self._pending_tl_track = pending - break - else: - self.core.tracklist._mark_unplayable(pending) + if not backend: + continue + + try: + if backend.playback.change_track(pending.track).get(): + self._pending_tl_track = pending + break + except Exception: + logger.exception('%s backend caused an exception.', + backend.actor_ref.actor_class.__name__) + + self.core.tracklist._mark_unplayable(pending) pending = self.core.tracklist.eot_track(pending) def _on_tracklist_change(self): diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 860ce556..cfd58793 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -333,6 +333,34 @@ class TestOnAboutToFinish(BaseTest): self.assertIn(tl_track, self.core.tracklist.tl_tracks) + def test_on_about_to_finish_skips_over_change_track_error(self): + # Trigger an exception in translate_uri. + track = Track(uri='dummy:error', length=1234) + self.core.tracklist.add(tracks=[track], at_position=1) + + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.trigger_about_to_finish() + + assert self.core.playback.get_current_tl_track() == tl_tracks[2] + + def test_on_about_to_finish_skips_over_change_track_unplayable(self): + # Makes translate_uri return None. + track = Track(uri='dummy:unplayable', length=1234) + self.core.tracklist.add(tracks=[track], at_position=1) + + tl_tracks = self.core.tracklist.get_tl_tracks() + + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.trigger_about_to_finish() + + assert self.core.playback.get_current_tl_track() == tl_tracks[2] + class TestConsumeHandling(BaseTest): From 494e29ebaf792c738860c00fbf089d54e0c09fed Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 22:28:55 +0100 Subject: [PATCH 22/39] docs: Remove docs for deprecated local/ configs --- docs/ext/local.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/ext/local.rst b/docs/ext/local.rst index ef9df5d7..1512524e 100644 --- a/docs/ext/local.rst +++ b/docs/ext/local.rst @@ -89,15 +89,6 @@ See :ref:`config` for general help on configuring Mopidy. Path to directory with local media files. -.. confval:: local/data_dir - - Path to directory to store local metadata such as libraries and playlists - in. - -.. confval:: local/playlists_dir - - Path to playlists directory with m3u files for local media. - .. confval:: local/scan_timeout Number of milliseconds before giving up scanning a file and moving on to From cd4e3fa37b596676dce641f7313bacc7016f269c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 22:36:38 +0100 Subject: [PATCH 23/39] docs: Tweak changelog --- docs/changelog.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 938e19d6..f48ff38a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -97,14 +97,14 @@ MPD frontend - Idle events are now emitted on ``seeked`` events. This fix means that clients relying on ``idle`` events now get notified about seeks. - (Fixes: :issue:`1331` :issue:`1347`) + (Fixes: :issue:`1331`, PR: :issue:`1347`) - Idle events are now emitted on ``playlists_loaded`` events. This fix means that clients relying on ``idle`` events now get notified about playlist loads. - (Fixes: :issue:`1331` PR: :issue:`1347`) + (Fixes: :issue:`1331`, PR: :issue:`1347`) - Event handler for ``playlist_deleted`` has been unbroken. This unreported bug - would cause the MPD Frontend to crash preventing any further communication + would cause the MPD frontend to crash preventing any further communication via the MPD protocol. (PR: :issue:`1347`) Zeroconf @@ -164,9 +164,9 @@ Audio argument is no longer in use and will be removed in the future. As far as we know, this is only used by Mopidy-Spotify. -- Duplicate seek events getting to AppSrc based backends is now fixed. This - should prevent seeking in Mopidy-Spotify from glitching. - (Fixes: :issue:`1404`) +- Duplicate seek events getting to ``appsrc`` based backends is now fixed. This + should prevent seeking in Mopidy-Spotify from glitching. (Fixes: + :issue:`1404`) - Workaround crash caused by a race that does not seem to affect functionality. This should be fixed properly together with :issue:`1222`. (Fixes: @@ -174,7 +174,7 @@ Audio - Add a new config option, :confval:`audio/buffer_time`, for setting the buffer time of the GStreamer queue. If you experience buffering before track - changes, it may help to increase this. Workaround for :issue:`1409`. + changes, it may help to increase this. (Workaround for :issue:`1409`) - ``tags_changed`` events are only emitted for fields that have changed. Previous behavior was to emit this for all fields received from GStreamer. @@ -186,6 +186,9 @@ Gapless - Add partial support for gapless playback. Gapless now works as long as you don't change tracks or use next/previous. (PR: :issue:`1288`) + The :ref:`streaming` docs has been updated with the workarounds still needed + to properly stream Mopidy audio through Icecast. + - Core playback has been refactored to better handle gapless, and async state changes. From f3c31538e6e6de6cc9197d065468a9e6cf9fbbf9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 22:39:31 +0100 Subject: [PATCH 24/39] audio: Remove unused 'capabilities' argument --- docs/changelog.rst | 6 +++--- mopidy/audio/utils.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f48ff38a..a251520c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -160,9 +160,9 @@ Audio If your Mopidy backend uses ``set_appsrc()``, please refer to GStreamer documentation for details on the new caps string format. -- **Deprecated:** :func:`mopidy.audio.utils.create_buffer`'s ``capabilities`` - argument is no longer in use and will be removed in the future. As far as we - know, this is only used by Mopidy-Spotify. +- **Breaking:** :func:`mopidy.audio.utils.create_buffer`'s ``capabilities`` + argument is no longer in use and has been removed. As far as we know, this + was only used by Mopidy-Spotify. - Duplicate seek events getting to ``appsrc`` based backends is now fixed. This should prevent seeking in Mopidy-Spotify from glitching. (Fixes: diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 8bc5279d..2027485a 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -10,13 +10,13 @@ def calculate_duration(num_samples, sample_rate): return Gst.util_uint64_scale(num_samples, Gst.SECOND, sample_rate) -def create_buffer(data, capabilites=None, timestamp=None, duration=None): +def create_buffer(data, timestamp=None, duration=None): """Create a new GStreamer buffer based on provided data. Mainly intended to keep gst imports out of non-audio modules. - .. versionchanged:: 1.2 - ``capabilites`` argument is no longer in use + .. versionchanged:: 2.0 + ``capabilites`` argument was removed. """ if not data: raise ValueError('Cannot create buffer without data') From 6407e87301522a31e738502b54105048c6f63cb9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 22:40:27 +0100 Subject: [PATCH 25/39] core: Update versionadded from 1.2 to 2.0 --- mopidy/core/playlists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py index 87790c25..3c17a898 100644 --- a/mopidy/core/playlists.py +++ b/mopidy/core/playlists.py @@ -39,7 +39,7 @@ class PlaylistsController(object): :rtype: list of string - .. versionadded:: 1.2 + .. versionadded:: 2.0 """ return list(sorted(self.backends.with_playlists.keys())) From 2d989c581dff74b7473ed3def2568a641af8c809 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 14 Feb 2016 22:57:12 +0100 Subject: [PATCH 26/39] docs: Summary of 2.0 release --- docs/changelog.rst | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a251520c..63774f3e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,13 +8,45 @@ This changelog is used to track all major changes to Mopidy. v2.0.0 (UNRELEASED) =================== -Feature release. +Mopidy 2.0 is here! + +Since the release of 1.1, we've closed or merged approximately 80 issues and +pull requests through about 350 commits by 14 extraordinary people, including +10 newcomers. That's about the same amount of issues and commits as between 1.0 +and 1.1. The number of contributors is a bit lower, but we didn't have a real +life sprint in this development cycle. Thanks to :ref:`everyone ` who +has :ref:`contributed `! + +With the release of Mopidy 1.0 we promised that any extension working with +Mopidy 1.0 should continue working with all Mopidy 1.x releases. Mopidy 2.0 is +quite a friendly major release, and will only break a single extension that we +know of: Mopidy-Spotify. To ensure that everything keeps on working, please +upgrade to Mopidy 2.0 and Mopidy-Spotify 3.0 at the same time. + +No deprecated functionality has been removed in Mopidy 2.0. + +The major features of Mopidy 2.0 are: + +- Gapless playback has been mostly implemented. It works as long as you don't + change tracks in the middle of a track or use previous and next. In a future + release previous and next will also become gapless. It is now quite easy to + have Mopidy streaming audio over the network using Icecast. See the updated + :ref:`streaming` docs for details of how to set it up and workarounds for the + remaining issues. + +- Mopidy has upgraded from GStreamer 0.10 to 1.x. This has been on our backlog + for more than three years. With this upgrade we're ridding ourselves with + years of GStreamer bugs that have been fixed in newer releases, we can get + into Debian testing again, and we've removed the last major roadblock for + running Mopidy on Python 3. Dependencies ------------ - Mopidy now requires GStreamer >= 1.2.3, as we've finally ported from - GStreamer 0.10. + GStreamer 0.10. Since we're requiring a new major version of our major + dependency, we're upping the major version of Mopidy too. (Fixes: + :issue:`225`) Core API -------- From a0c0ab4cde76667bc5a75fb403a8ca65bce2687c Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Sun, 14 Feb 2016 22:34:03 +0000 Subject: [PATCH 27/39] docs: changelog minor rewording --- docs/changelog.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 63774f3e..b7234100 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,14 +13,14 @@ Mopidy 2.0 is here! Since the release of 1.1, we've closed or merged approximately 80 issues and pull requests through about 350 commits by 14 extraordinary people, including 10 newcomers. That's about the same amount of issues and commits as between 1.0 -and 1.1. The number of contributors is a bit lower, but we didn't have a real -life sprint in this development cycle. Thanks to :ref:`everyone ` who -has :ref:`contributed `! +and 1.1. The number of contributors is a bit lower but we didn't have a real +life sprint during this development cycle. Thanks to :ref:`everyone ` +who has :ref:`contributed `! With the release of Mopidy 1.0 we promised that any extension working with Mopidy 1.0 should continue working with all Mopidy 1.x releases. Mopidy 2.0 is -quite a friendly major release, and will only break a single extension that we -know of: Mopidy-Spotify. To ensure that everything keeps on working, please +quite a friendly major release and will only break a single extension that we +know of: Mopidy-Spotify. To ensure that everything continues working, please upgrade to Mopidy 2.0 and Mopidy-Spotify 3.0 at the same time. No deprecated functionality has been removed in Mopidy 2.0. @@ -29,13 +29,13 @@ The major features of Mopidy 2.0 are: - Gapless playback has been mostly implemented. It works as long as you don't change tracks in the middle of a track or use previous and next. In a future - release previous and next will also become gapless. It is now quite easy to + release, previous and next will also become gapless. It is now quite easy to have Mopidy streaming audio over the network using Icecast. See the updated :ref:`streaming` docs for details of how to set it up and workarounds for the remaining issues. - Mopidy has upgraded from GStreamer 0.10 to 1.x. This has been on our backlog - for more than three years. With this upgrade we're ridding ourselves with + for more than three years. With this upgrade we're ridding ourselves of years of GStreamer bugs that have been fixed in newer releases, we can get into Debian testing again, and we've removed the last major roadblock for running Mopidy on Python 3. From 9296ddd75b0e287a93a3054089c75ae5bfcbe4f6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 14 Feb 2016 23:47:51 +0100 Subject: [PATCH 28/39] stream: Update playback tests to include backend --- tests/stream/test_playback.py | 44 +++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/stream/test_playback.py b/tests/stream/test_playback.py index ef7da0bf..4ff684ca 100644 --- a/tests/stream/test_playback.py +++ b/tests/stream/test_playback.py @@ -4,6 +4,8 @@ import mock import pytest +import requests.exceptions + import responses from mopidy import exceptions @@ -27,6 +29,11 @@ def config(): 'proxy': {}, 'stream': { 'timeout': TIMEOUT, + 'metadata_blacklist': [], + 'protocols': ['file'], + }, + 'file': { + 'enabled': False }, } @@ -36,24 +43,21 @@ def audio(): return mock.Mock() -@pytest.fixture +@pytest.yield_fixture def scanner(): - scan_mock = mock.Mock(spec=scan.Scanner) - scan_mock.scan.return_value = None - return scan_mock + patcher = mock.patch.object(scan, 'Scanner') + yield patcher.start()() + patcher.stop() @pytest.fixture -def backend(scanner): - backend = mock.Mock() - backend.uri_schemes = ['file'] - backend._scanner = scanner - return backend +def backend(audio, config, scanner): + return actor.StreamBackend(audio=audio, config=config) @pytest.fixture -def provider(audio, backend, config): - return actor.StreamPlaybackProvider(audio, backend, config) +def provider(backend): + return backend.playback class TestTranslateURI(object): @@ -184,14 +188,24 @@ class TestTranslateURI(object): % STREAM_URI in caplog.text()) assert result == STREAM_URI - def test_failed_download_returns_none(self, provider, caplog): - with mock.patch.object(actor, 'http') as http_mock: - http_mock.download.return_value = None + @responses.activate + def test_failed_download_returns_none(self, scanner, provider, caplog): + scanner.scan.side_effect = [ + mock.Mock(mime='text/foo', playable=False) + ] - result = provider.translate_uri(PLAYLIST_URI) + responses.add( + responses.GET, PLAYLIST_URI, + body=requests.exceptions.HTTPError('Kaboom')) + + result = provider.translate_uri(PLAYLIST_URI) assert result is None + assert ( + 'Unwrapping stream from URI (%s) failed: ' + 'error downloading URI' % PLAYLIST_URI) in caplog.text() + @responses.activate def test_playlist_references_itself(self, scanner, provider, caplog): scanner.scan.side_effect = [ From a6495e0ecdb0187c3ec0bed038b01a2fe55227d2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 14 Feb 2016 23:49:05 +0100 Subject: [PATCH 29/39] stream: Update library tests to include backend --- tests/stream/test_library.py | 52 +++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/tests/stream/test_library.py b/tests/stream/test_library.py index 67053924..682c98ab 100644 --- a/tests/stream/test_library.py +++ b/tests/stream/test_library.py @@ -4,7 +4,6 @@ import mock import pytest -from mopidy.audio import scan from mopidy.internal import path from mopidy.models import Track from mopidy.stream import actor @@ -13,16 +12,23 @@ from tests import path_to_data_dir @pytest.fixture -def scanner(): - return scan.Scanner(timeout=100, proxy_config={}) +def config(): + return { + 'proxy': {}, + 'stream': { + 'timeout': 1000, + 'metadata_blacklist': [], + 'protocols': ['file'], + }, + 'file': { + 'enabled': False + }, + } @pytest.fixture -def backend(scanner): - backend = mock.Mock() - backend.uri_schemes = ['file'] - backend._scanner = scanner - return backend +def audio(): + return mock.Mock() @pytest.fixture @@ -30,26 +36,28 @@ def track_uri(): return path.path_to_uri(path_to_data_dir('song1.wav')) -def test_lookup_ignores_unknown_scheme(backend): - library = actor.StreamLibraryProvider(backend, []) - - assert library.lookup('http://example.com') == [] +def test_lookup_ignores_unknown_scheme(audio, config): + backend = actor.StreamBackend(audio=audio, config=config) + backend.library.lookup('http://example.com') == [] -def test_lookup_respects_blacklist(backend, track_uri): - library = actor.StreamLibraryProvider(backend, [track_uri]) +def test_lookup_respects_blacklist(audio, config, track_uri): + config['stream']['metadata_blacklist'].append(track_uri) + backend = actor.StreamBackend(audio=audio, config=config) - assert library.lookup(track_uri) == [Track(uri=track_uri)] + assert backend.library.lookup(track_uri) == [Track(uri=track_uri)] -def test_lookup_respects_blacklist_globbing(backend, track_uri): - blacklist = [path.path_to_uri(path_to_data_dir('')) + '*'] - library = actor.StreamLibraryProvider(backend, blacklist) +def test_lookup_respects_blacklist_globbing(audio, config, track_uri): + blacklist_glob = path.path_to_uri(path_to_data_dir('')) + '*' + config['stream']['metadata_blacklist'].append(blacklist_glob) + backend = actor.StreamBackend(audio=audio, config=config) - assert library.lookup(track_uri) == [Track(uri=track_uri)] + assert backend.library.lookup(track_uri) == [Track(uri=track_uri)] -def test_lookup_converts_uri_metadata_to_track(backend, track_uri): - library = actor.StreamLibraryProvider(backend, []) +def test_lookup_converts_uri_metadata_to_track(audio, config, track_uri): + backend = actor.StreamBackend(audio=audio, config=config) - assert library.lookup(track_uri) == [Track(length=4406, uri=track_uri)] + result = backend.library.lookup(track_uri) + assert result == [Track(length=4406, uri=track_uri)] From 9aa2a8a370bcaf6e1844966db094ab23151fb375 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 14 Feb 2016 23:50:18 +0100 Subject: [PATCH 30/39] stream: Start moving state up to backend This allows us to start unifying how we handle playlists in the library and playback cases. --- mopidy/stream/actor.py | 43 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index c2e39652..b42985d0 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -25,10 +25,19 @@ class StreamBackend(pykka.ThreadingActor, backend.Backend): timeout=config['stream']['timeout'], proxy_config=config['proxy']) - self.library = StreamLibraryProvider( - backend=self, blacklist=config['stream']['metadata_blacklist']) - self.playback = StreamPlaybackProvider( - audio=audio, backend=self, config=config) + self._session = http.get_requests_session( + proxy_config=config['proxy'], + user_agent='%s/%s' % ( + stream.Extension.dist_name, stream.Extension.version)) + + blacklist = config['stream']['metadata_blacklist'] + self._blacklist_re = re.compile( + r'^(%s)$' % '|'.join(fnmatch.translate(u) for u in blacklist)) + + self._timeout = config['stream']['timeout'] + + self.library = StreamLibraryProvider(backend=self) + self.playback = StreamPlaybackProvider(audio=audio, backend=self) self.playlists = None self.uri_schemes = audio_lib.supported_uri_schemes( @@ -43,23 +52,16 @@ class StreamBackend(pykka.ThreadingActor, backend.Backend): class StreamLibraryProvider(backend.LibraryProvider): - - def __init__(self, backend, blacklist): - super(StreamLibraryProvider, self).__init__(backend) - self._scanner = backend._scanner - self._blacklist_re = re.compile( - r'^(%s)$' % '|'.join(fnmatch.translate(u) for u in blacklist)) - def lookup(self, uri): if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes: return [] - if self._blacklist_re.match(uri): + if self.backend._blacklist_re.match(uri): logger.debug('URI matched metadata lookup blacklist: %s', uri) return [Track(uri=uri)] try: - result = self._scanner.scan(uri) + result = self.backend._scanner.scan(uri) track = tags.convert_tags_to_track(result.tags).replace( uri=uri, length=result.duration) except exceptions.ScannerError as e: @@ -71,21 +73,12 @@ class StreamLibraryProvider(backend.LibraryProvider): class StreamPlaybackProvider(backend.PlaybackProvider): - def __init__(self, audio, backend, config): - super(StreamPlaybackProvider, self).__init__(audio, backend) - self._config = config - self._scanner = backend._scanner - self._session = http.get_requests_session( - proxy_config=config['proxy'], - user_agent='%s/%s' % ( - stream.Extension.dist_name, stream.Extension.version)) - def translate_uri(self, uri): return _unwrap_stream( uri, - timeout=self._config['stream']['timeout'], - scanner=self._scanner, - requests_session=self._session) + timeout=self.backend._timeout, + scanner=self.backend._scanner, + requests_session=self.backend._session) def _unwrap_stream(uri, timeout, scanner, requests_session): From ce81b362dc338d7c1216b68183d46e59051d7b97 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 15 Feb 2016 00:00:30 +0100 Subject: [PATCH 31/39] stream: Add scheme and blacklist check to translate_uri We don't bother with this inside the unwrap code as if something redirects us so be it. --- mopidy/stream/actor.py | 7 +++++++ tests/stream/test_playback.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index b42985d0..ff99264d 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -74,6 +74,13 @@ class StreamLibraryProvider(backend.LibraryProvider): class StreamPlaybackProvider(backend.PlaybackProvider): def translate_uri(self, uri): + if urllib.parse.urlsplit(uri).scheme not in self.backend.uri_schemes: + return None + + if self.backend._blacklist_re.match(uri): + logger.debug('URI matched metadata lookup blacklist: %s', uri) + return uri + return _unwrap_stream( uri, timeout=self.backend._timeout, diff --git a/tests/stream/test_playback.py b/tests/stream/test_playback.py index 4ff684ca..1816f73e 100644 --- a/tests/stream/test_playback.py +++ b/tests/stream/test_playback.py @@ -30,7 +30,7 @@ def config(): 'stream': { 'timeout': TIMEOUT, 'metadata_blacklist': [], - 'protocols': ['file'], + 'protocols': ['http'], }, 'file': { 'enabled': False From 2e5cfba710d1f8c9df39b0c4eaf8b7f66ff59ad4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 15 Feb 2016 00:05:01 +0100 Subject: [PATCH 32/39] stream: Make library lookup use stream unwrapping (fixes #1445) --- mopidy/stream/actor.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index ff99264d..f58b59ec 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -60,12 +60,17 @@ class StreamLibraryProvider(backend.LibraryProvider): logger.debug('URI matched metadata lookup blacklist: %s', uri) return [Track(uri=uri)] - try: - result = self.backend._scanner.scan(uri) + result = _unwrap_stream( + uri, + timeout=self.backend._timeout, + scanner=self.backend._scanner, + requests_session=self.backend._session)[1] + + if result: track = tags.convert_tags_to_track(result.tags).replace( uri=uri, length=result.duration) - except exceptions.ScannerError as e: - logger.warning('Problem looking up %s: %s', uri, e) + else: + logger.warning('Problem looking up %s: %s', uri) track = Track(uri=uri) return [track] @@ -85,9 +90,10 @@ class StreamPlaybackProvider(backend.PlaybackProvider): uri, timeout=self.backend._timeout, scanner=self.backend._scanner, - requests_session=self.backend._session) + requests_session=self.backend._session)[0] +# TODO: cleanup the return value of this. def _unwrap_stream(uri, timeout, scanner, requests_session): """ Get a stream URI from a playlist URI, ``uri``. @@ -105,7 +111,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): logger.info( 'Unwrapping stream from URI (%s) failed: ' 'playlist referenced itself', uri) - return None + return None, None else: seen_uris.add(uri) @@ -117,7 +123,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): logger.info( 'Unwrapping stream from URI (%s) failed: ' 'timed out in %sms', uri, timeout) - return None + return None, None scan_result = scanner.scan(uri, timeout=scan_timeout) except exceptions.ScannerError as exc: logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc) @@ -130,14 +136,14 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): ): logger.debug( 'Unwrapped potential %s stream: %s', scan_result.mime, uri) - return uri + return uri, scan_result download_timeout = deadline - time.time() if download_timeout < 0: logger.info( 'Unwrapping stream from URI (%s) failed: timed out in %sms', uri, timeout) - return None + return None, None content = http.download( requests_session, uri, timeout=download_timeout) @@ -145,14 +151,14 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): logger.info( 'Unwrapping stream from URI (%s) failed: ' 'error downloading URI %s', original_uri, uri) - return None + return None, None uris = playlists.parse(content) if not uris: logger.debug( 'Failed parsing URI (%s) as playlist; found potential stream.', uri) - return uri + return uri, None # TODO Test streams and return first that seems to be playable logger.debug( From 5c1a4c66f2cca8a790eb15dc2e66d8239a14b3d2 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Sun, 14 Feb 2016 23:06:11 +0000 Subject: [PATCH 33/39] docs: final pedantic changelog wording change. --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b7234100..c0b74dd0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -34,7 +34,7 @@ The major features of Mopidy 2.0 are: :ref:`streaming` docs for details of how to set it up and workarounds for the remaining issues. -- Mopidy has upgraded from GStreamer 0.10 to 1.x. This has been on our backlog +- Mopidy has upgraded from GStreamer 0.10 to 1.x. This has been in our backlog for more than three years. With this upgrade we're ridding ourselves of years of GStreamer bugs that have been fixed in newer releases, we can get into Debian testing again, and we've removed the last major roadblock for From f53a0d220051016bc348cb344f61026fb0d1e866 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 15 Feb 2016 20:46:43 +0100 Subject: [PATCH 34/39] stream: Address review comments for PR#1447 --- mopidy/stream/actor.py | 23 ++++++++++------------- tests/stream/test_library.py | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index f58b59ec..0861b5b0 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -60,15 +60,13 @@ class StreamLibraryProvider(backend.LibraryProvider): logger.debug('URI matched metadata lookup blacklist: %s', uri) return [Track(uri=uri)] - result = _unwrap_stream( - uri, - timeout=self.backend._timeout, - scanner=self.backend._scanner, - requests_session=self.backend._session)[1] + _, scan_result = _unwrap_stream( + uri, timeout=self.backend._timeout, scanner=self.backend._scanner, + requests_session=self.backend._session) - if result: - track = tags.convert_tags_to_track(result.tags).replace( - uri=uri, length=result.duration) + if scan_result: + track = tags.convert_tags_to_track(scan_result.tags).replace( + uri=uri, length=scan_result.duration) else: logger.warning('Problem looking up %s: %s', uri) track = Track(uri=uri) @@ -86,11 +84,10 @@ class StreamPlaybackProvider(backend.PlaybackProvider): logger.debug('URI matched metadata lookup blacklist: %s', uri) return uri - return _unwrap_stream( - uri, - timeout=self.backend._timeout, - scanner=self.backend._scanner, - requests_session=self.backend._session)[0] + unwrapped_uri, _ = _unwrap_stream( + uri, timeout=self.backend._timeout, scanner=self.backend._scanner, + requests_session=self.backend._session) + return unwrapped_uri # TODO: cleanup the return value of this. diff --git a/tests/stream/test_library.py b/tests/stream/test_library.py index 682c98ab..29348a6c 100644 --- a/tests/stream/test_library.py +++ b/tests/stream/test_library.py @@ -38,7 +38,7 @@ def track_uri(): def test_lookup_ignores_unknown_scheme(audio, config): backend = actor.StreamBackend(audio=audio, config=config) - backend.library.lookup('http://example.com') == [] + assert backend.library.lookup('http://example.com') == [] def test_lookup_respects_blacklist(audio, config, track_uri): From 3f0d7b96d09d3210e45baf4d2dbd9600d43f5b89 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 15 Feb 2016 20:53:05 +0100 Subject: [PATCH 35/39] docs: Add stream backend to changelog --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b7234100..c7836f09 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -110,6 +110,12 @@ M3U backend - Improve reliability of playlist updates using the core playlist API by applying the write-replace pattern for file updates. +Stream backend +-------------- + +- Make sure both lookup and playback correctly handle playlists and our + blacklist support. (Fixes: :issue:`1445`, PR: :issue:`1447`) + MPD frontend ------------ From 665eccda932f5c21ca04450909fe07544308c39d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 15 Feb 2016 21:41:31 +0100 Subject: [PATCH 36/39] docs: Add v2.0.0 release date --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e43e8973..7dc4a747 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog This changelog is used to track all major changes to Mopidy. -v2.0.0 (UNRELEASED) +v2.0.0 (2016-02-15) =================== Mopidy 2.0 is here! From 32c135a12470e9641acd322cfcd7d275259e7c7d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 15 Feb 2016 22:14:14 +0100 Subject: [PATCH 37/39] docs: Add changelog section for 2.1.0 --- docs/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7dc4a747..6bd55372 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,14 @@ Changelog This changelog is used to track all major changes to Mopidy. +v2.1.0 (UNRELEASED) +=================== + +Feature release. + +- Nothing yet. + + v2.0.0 (2016-02-15) =================== From 0a4e1b15c8b7d07a0f85f70946b0392ecb7120cd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 15 Feb 2016 22:15:18 +0100 Subject: [PATCH 38/39] docs: Add changelog section for 2.0.1 --- docs/changelog.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7dc4a747..73dad36d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,14 @@ Changelog This changelog is used to track all major changes to Mopidy. +v2.0.1 (UNRELEASED) +=================== + +Bug fix release. + +- Nothing yet. + + v2.0.0 (2016-02-15) =================== From 23098d7d935fef7d253425b51524367898352b9e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 15 Feb 2016 23:44:48 +0100 Subject: [PATCH 39/39] docs: Fix broken link --- docs/installation/raspberrypi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/raspberrypi.rst b/docs/installation/raspberrypi.rst index 6d2dd3cd..f62bb124 100644 --- a/docs/installation/raspberrypi.rst +++ b/docs/installation/raspberrypi.rst @@ -68,7 +68,7 @@ How to for Raspbian Jessie #. Finally, you need to set a couple of :doc:`config values `, and then you're ready to :doc:`run Mopidy `. Alternatively you may - want to have Mopidy run as a :doc:`system service `, automatically + want to have Mopidy run as a :ref:`system service `, automatically starting at boot.