From 0a4e1b15c8b7d07a0f85f70946b0392ecb7120cd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 15 Feb 2016 22:15:18 +0100 Subject: [PATCH 01/46] 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 1e8bef25d36e1c257a369ff0ce5729c2939f384e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 22 Feb 2016 12:42:56 +0100 Subject: [PATCH 02/46] audio: Set soft-volume flag on playbin --- docs/changelog.rst | 5 ++++- mopidy/audio/actor.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 73dad36d..dad0c741 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,7 +10,10 @@ v2.0.1 (UNRELEASED) Bug fix release. -- Nothing yet. +- Audio: Set ``soft-volume`` flag on GStreamer's playbin element. This is the + playbin's default, but we managed to override it when configuring the playbin + to only process audio. This should fix the "Volume/mute is not available" + warning. v2.0.0 (2016-02-15) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 64300ff9..267b228d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -21,6 +21,9 @@ logger = logging.getLogger(__name__) # set_state() on a pipeline. gst_logger = logging.getLogger('mopidy.audio.gst') +_GST_PLAY_FLAGS_AUDIO = 0x02 +_GST_PLAY_FLAGS_SOFT_VOLUME = 0x10 + _GST_STATE_MAPPING = { Gst.State.PLAYING: PlaybackState.PLAYING, Gst.State.PAUSED: PlaybackState.PAUSED, @@ -448,7 +451,8 @@ class Audio(pykka.ThreadingActor): def _setup_playbin(self): playbin = Gst.ElementFactory.make('playbin') - playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO + playbin.set_property( + 'flags', _GST_PLAY_FLAGS_AUDIO | _GST_PLAY_FLAGS_SOFT_VOLUME) # TODO: turn into config values... playbin.set_property('buffer-size', 5 << 20) # 5MB From f0788515cd4b207b127142452004389f0211a80b Mon Sep 17 00:00:00 2001 From: Jens Luetjen Date: Sat, 27 Feb 2016 11:24:04 +0100 Subject: [PATCH 03/46] Find images in audio files Handle Gst.Sample as image in audio file tags (scaned with Gst1.0). --- mopidy/audio/tags.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 38a0bac9..a5714567 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -53,6 +53,12 @@ gstreamer-GstTagList.html result[tag].append(value.decode('utf-8', 'replace')) elif isinstance(value, (compat.text_type, bool, numbers.Number)): result[tag].append(value) + elif isinstance(value, Gst.Sample): + buf = value.get_buffer() + (found, mapinfo) = buf.map(Gst.MapFlags.READ) + if found: + result[tag].append(bytes(mapinfo.data)) + buf.unmap(mapinfo) else: logger.log( log.TRACE_LOG_LEVEL, From 6faa19dbf3e444ed3a2372de399e069d8d30b5bb Mon Sep 17 00:00:00 2001 From: Jens Luetjen Date: Mon, 29 Feb 2016 19:40:49 +0100 Subject: [PATCH 04/46] Always unmap mapped buffer. Always unmap the mapped memory, even in case of exception. --- mopidy/audio/tags.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index a5714567..1625ccc4 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -57,8 +57,10 @@ gstreamer-GstTagList.html buf = value.get_buffer() (found, mapinfo) = buf.map(Gst.MapFlags.READ) if found: - result[tag].append(bytes(mapinfo.data)) - buf.unmap(mapinfo) + try: + result[tag].append(bytes(mapinfo.data)) + finally: + buf.unmap(mapinfo) else: logger.log( log.TRACE_LOG_LEVEL, From 64a58e0662898c6e3d408fdfd9882ed61c98f21b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 25 Mar 2016 13:09:44 +0100 Subject: [PATCH 05/46] audio: Move sample to data conversion to a helper. Also add check for None buffer being returned. --- mopidy/audio/tags.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 1625ccc4..e29f37d8 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -54,13 +54,9 @@ gstreamer-GstTagList.html elif isinstance(value, (compat.text_type, bool, numbers.Number)): result[tag].append(value) elif isinstance(value, Gst.Sample): - buf = value.get_buffer() - (found, mapinfo) = buf.map(Gst.MapFlags.READ) - if found: - try: - result[tag].append(bytes(mapinfo.data)) - finally: - buf.unmap(mapinfo) + data = _extract_sample_data(value) + if data: + result[tag].append(data) else: logger.log( log.TRACE_LOG_LEVEL, @@ -70,6 +66,19 @@ gstreamer-GstTagList.html return result +def _extract_sample_data(sample): + buf = sample.get_buffer() + if not buf: + return None + found, mapinfo = buf.map(Gst.MapFlags.READ) + if not found: + return None + try: + return bytes(mapinfo.data) + finally: + buf.unmap(mapinfo) + + # TODO: split based on "stream" and "track" based conversion? i.e. handle data # from radios in it's own helper instead? def convert_tags_to_track(tags): From 793e42531476667db6b670d799b5baf3ebc8365f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 25 Mar 2016 13:11:07 +0100 Subject: [PATCH 06/46] audio: Make buffer conversion work on older GStreamer installs --- mopidy/audio/tags.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index e29f37d8..5ae86468 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -70,13 +70,7 @@ def _extract_sample_data(sample): buf = sample.get_buffer() if not buf: return None - found, mapinfo = buf.map(Gst.MapFlags.READ) - if not found: - return None - try: - return bytes(mapinfo.data) - finally: - buf.unmap(mapinfo) + return buf.extract_dup(0, buf.get_size()) # TODO: split based on "stream" and "track" based conversion? i.e. handle data From 3c535409adadb51eacf7e2f1df9196d493413bfe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 25 Mar 2016 14:34:15 +0100 Subject: [PATCH 07/46] docs: Add buffer fix to changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index dad0c741..f8f7e43b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ Bug fix release. to only process audio. This should fix the "Volume/mute is not available" warning. +- Audio: Fix buffer conversion. This fixes image extraction. + (PR: :issue:`1472`) + v2.0.0 (2016-02-15) =================== From 3183f43b18955a55b41b953502e49b67229ecf0f Mon Sep 17 00:00:00 2001 From: SeeSpotRun Date: Tue, 15 Mar 2016 14:42:29 +1000 Subject: [PATCH 08/46] scanner: workaround for gstreamer not pushing tags before PREROLL refer https://bugzilla.gnome.org/show_bug.cgi?id=763553 --- mopidy/audio/scan.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c63405b0..1a199e58 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -210,8 +210,11 @@ def _process(pipeline, timeout_ms): elif message.type == Gst.MessageType.EOS: return tags, mime, have_audio elif message.type == Gst.MessageType.ASYNC_DONE: - if message.src == pipeline: + if tags: return tags, mime, have_audio + else: + # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: + pipeline.set_state(Gst.State.PLAYING) elif message.type == Gst.MessageType.TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. @@ -220,6 +223,9 @@ def _process(pipeline, timeout_ms): now = int(time.time() * 1000) timeout -= now - previous previous = now + # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: + if tags: + pipeline.set_state(Gst.State.PAUSED) raise exceptions.ScannerError('Timeout after %dms' % timeout_ms) From 57518861ea55546d8f491448ff65453d2cf0d184 Mon Sep 17 00:00:00 2001 From: SeeSpotRun Date: Tue, 15 Mar 2016 18:37:23 +1000 Subject: [PATCH 09/46] scanner: move _query_duration() into _process() --- mopidy/audio/scan.py | 59 +++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 1a199e58..4642c3f2 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -61,8 +61,7 @@ class Scanner(object): try: _start_pipeline(pipeline) - tags, mime, have_audio = _process(pipeline, timeout) - duration = _query_duration(pipeline) + tags, mime, have_audio, duration = _process(pipeline, timeout) seekable = _query_seekable(pipeline) finally: signals.clear() @@ -136,30 +135,6 @@ def _start_pipeline(pipeline): pipeline.set_state(Gst.State.PLAYING) -def _query_duration(pipeline, timeout=100): - # 1. Try and get a duration, return if success. - # 2. Some formats need to play some buffers before duration is found. - # 3. Wait for a duration change event. - # 4. Try and get a duration again. - - success, duration = pipeline.query_duration(Gst.Format.TIME) - if success and duration >= 0: - return duration // Gst.MSECOND - - result = pipeline.set_state(Gst.State.PLAYING) - if result == Gst.StateChangeReturn.FAILURE: - return None - - gst_timeout = timeout * Gst.MSECOND - bus = pipeline.get_bus() - bus.timed_pop_filtered(gst_timeout, Gst.MessageType.DURATION_CHANGED) - - success, duration = pipeline.query_duration(Gst.Format.TIME) - if success and duration >= 0: - return duration // Gst.MSECOND - return None - - def _query_seekable(pipeline): query = Gst.Query.new_seeking(Gst.Format.TIME) pipeline.query(query) @@ -172,6 +147,7 @@ def _process(pipeline, timeout_ms): mime = None have_audio = False missing_message = None + duration = None types = ( Gst.MessageType.ELEMENT | @@ -179,6 +155,7 @@ def _process(pipeline, timeout_ms): Gst.MessageType.ERROR | Gst.MessageType.EOS | Gst.MessageType.ASYNC_DONE | + Gst.MessageType.DURATION_CHANGED | Gst.MessageType.TAG ) @@ -197,7 +174,7 @@ def _process(pipeline, timeout_ms): mime = message.get_structure().get_value('caps').get_name() if mime and ( mime.startswith('text/') or mime == 'application/xml'): - return tags, mime, have_audio + return tags, mime, have_audio, duration elif message.get_structure().get_name() == 'have-audio': have_audio = True elif message.type == Gst.MessageType.ERROR: @@ -205,16 +182,28 @@ def _process(pipeline, timeout_ms): if missing_message and not mime: caps = missing_message.get_structure().get_value('detail') mime = caps.get_structure(0).get_name() - return tags, mime, have_audio + return tags, mime, have_audio, duration raise exceptions.ScannerError(error) elif message.type == Gst.MessageType.EOS: - return tags, mime, have_audio + return tags, mime, have_audio, duration elif message.type == Gst.MessageType.ASYNC_DONE: - if tags: - return tags, mime, have_audio + success, duration = pipeline.query_duration(Gst.Format.TIME) + if success: + duration = duration // Gst.MSECOND else: - # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: - pipeline.set_state(Gst.State.PLAYING) + duration = None + if (tags and duration + # workaround for + # https://bugzilla.gnome.org/show_bug.cgi?id=763553: + # try to start pipeline playing; if it doesn't then + # give up: + ) or ( pipeline.set_state(Gst.State.PLAYING) + == Gst.StateChangeReturn.FAILURE): + return tags, mime, have_audio, duration + elif message.type == Gst.MessageType.DURATION_CHANGED: + # duration will be read after ASYNC_DONE received; for now + # just give it a non-None value to flag that we have a duration: + duration = 0 elif message.type == Gst.MessageType.TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. @@ -223,8 +212,10 @@ def _process(pipeline, timeout_ms): now = int(time.time() * 1000) timeout -= now - previous previous = now + # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: - if tags: + # if we got what we want then stop playing (and wait for ASYNC_DONE) + if tags and duration: pipeline.set_state(Gst.State.PAUSED) raise exceptions.ScannerError('Timeout after %dms' % timeout_ms) From 6379f768890e6e412ade1889e0fc2cc25e4425f2 Mon Sep 17 00:00:00 2001 From: SeeSpotRun Date: Tue, 15 Mar 2016 18:40:25 +1000 Subject: [PATCH 10/46] scanner: minor simplification of timeout calculations --- mopidy/audio/scan.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 4642c3f2..162d1916 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -160,7 +160,7 @@ def _process(pipeline, timeout_ms): ) timeout = timeout_ms - previous = int(time.time() * 1000) + start = int(time.time() * 1000) while timeout > 0: message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types) @@ -209,9 +209,7 @@ def _process(pipeline, timeout_ms): # Note that this will only keep the last tag. tags.update(tags_lib.convert_taglist(taglist)) - now = int(time.time() * 1000) - timeout -= now - previous - previous = now + timeout = timeout_ms - ( int(time.time() * 1000) - start ) # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: # if we got what we want then stop playing (and wait for ASYNC_DONE) From 2d6f00b9124adb8a38ea32f3d3e8669bb9c4ecf6 Mon Sep 17 00:00:00 2001 From: SeeSpotRun Date: Tue, 15 Mar 2016 18:59:06 +1000 Subject: [PATCH 11/46] scanner: formatting to make travis / tox / flake8 happy --- mopidy/audio/scan.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 162d1916..99b14c82 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -197,8 +197,8 @@ def _process(pipeline, timeout_ms): # https://bugzilla.gnome.org/show_bug.cgi?id=763553: # try to start pipeline playing; if it doesn't then # give up: - ) or ( pipeline.set_state(Gst.State.PLAYING) - == Gst.StateChangeReturn.FAILURE): + ) or (pipeline.set_state(Gst.State.PLAYING) == + Gst.StateChangeReturn.FAILURE): return tags, mime, have_audio, duration elif message.type == Gst.MessageType.DURATION_CHANGED: # duration will be read after ASYNC_DONE received; for now @@ -209,7 +209,7 @@ def _process(pipeline, timeout_ms): # Note that this will only keep the last tag. tags.update(tags_lib.convert_taglist(taglist)) - timeout = timeout_ms - ( int(time.time() * 1000) - start ) + timeout = timeout_ms - (int(time.time() * 1000) - start) # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: # if we got what we want then stop playing (and wait for ASYNC_DONE) From c972ecd1f26431adfa01a4ac32d77223d01fed52 Mon Sep 17 00:00:00 2001 From: SeeSpotRun Date: Wed, 16 Mar 2016 08:42:34 +1000 Subject: [PATCH 12/46] scanner: fix newby logic error --- mopidy/audio/scan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 99b14c82..ff96b641 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -192,7 +192,7 @@ def _process(pipeline, timeout_ms): duration = duration // Gst.MSECOND else: duration = None - if (tags and duration + if (tags and duration is not None # workaround for # https://bugzilla.gnome.org/show_bug.cgi?id=763553: # try to start pipeline playing; if it doesn't then @@ -213,7 +213,7 @@ def _process(pipeline, timeout_ms): # workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553: # if we got what we want then stop playing (and wait for ASYNC_DONE) - if tags and duration: + if tags and duration is not None: pipeline.set_state(Gst.State.PAUSED) raise exceptions.ScannerError('Timeout after %dms' % timeout_ms) From e9137e132a04baf9d36ee3f34bf682c8ce3335d8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 25 Mar 2016 15:17:02 +0100 Subject: [PATCH 13/46] audio: Try and simplify logic for going to playing in scanner --- mopidy/audio/scan.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ff96b641..27888638 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -192,14 +192,18 @@ def _process(pipeline, timeout_ms): duration = duration // Gst.MSECOND else: duration = None - if (tags and duration is not None - # workaround for - # https://bugzilla.gnome.org/show_bug.cgi?id=763553: - # try to start pipeline playing; if it doesn't then - # give up: - ) or (pipeline.set_state(Gst.State.PLAYING) == - Gst.StateChangeReturn.FAILURE): + + if tags and duration is not None: return tags, mime, have_audio, duration + + # Workaround for upstream bug which causes tags/duration to arrive + # after pre-roll. We get around this by starting to play the track + # and then waiting for a duration change. + # https://bugzilla.gnome.org/show_bug.cgi?id=763553 + result = pipeline.set_state(Gst.State.PLAYING) + if result == Gst.StateChangeReturn.FAILURE: + return tags, mime, have_audio, duration + elif message.type == Gst.MessageType.DURATION_CHANGED: # duration will be read after ASYNC_DONE received; for now # just give it a non-None value to flag that we have a duration: From accd6bd1a9957ec89d591c599b7902b6d96deb5e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 25 Mar 2016 15:18:13 +0100 Subject: [PATCH 14/46] docs: Add duration fix to changelog. --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index f8f7e43b..2e9ca56a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,11 @@ Bug fix release. - Audio: Fix buffer conversion. This fixes image extraction. (PR: :issue:`1472`) +- Audio: Update scan logic to workaround GStreamer issue where tags and + duration might only be available after we start playing. + (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480` + PR: :issue:`1487`) + v2.0.0 (2016-02-15) =================== From a2f0d6960ff0e1f02d15f201cb37133b7aadc83a Mon Sep 17 00:00:00 2001 From: Jens Luetjen Date: Wed, 17 Feb 2016 22:54:22 +0100 Subject: [PATCH 15/46] Avoid endless loop if all tracks are unplayable. Limit the number of tries for changing to the nest track. The limit is 2 * tracklist length get all tracks in a shuffled playlist. --- mopidy/core/playback.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index d6c470f2..0d945b1c 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -252,22 +252,27 @@ class PlaybackController(object): return 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 not backend: - continue + # avoid endless loop if 'repeat' is 'true' and no track is playable + # * 2 -> second run to get all playable track in a shuffled playlist + count = self.core.tracklist.get_length() * 2 - 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__) + while pending: + backend = self._get_backend(pending) + if backend: + 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) + count -= 1 + if not count: + logger.info('No playable track in the list.') + break def _on_tracklist_change(self): """ @@ -352,6 +357,9 @@ class PlaybackController(object): current = self._pending_tl_track or self._current_tl_track pending = tl_track or current or self.core.tracklist.next_track(None) + # avoid endless loop if 'repeat' is 'true' and no track is playable + # * 2 -> second run to get all playable track in a shuffled playlist + count = self.core.tracklist.get_length() * 2 while pending: if self._change(pending, PlaybackState.PLAYING): @@ -360,6 +368,10 @@ class PlaybackController(object): self.core.tracklist._mark_unplayable(pending) current = pending pending = self.core.tracklist.next_track(current) + count -= 1 + if not count: + logger.info('No playable track in the list.') + break # TODO return result? From 6116705c1be9c3d39281991f39b3bfb417d9ed36 Mon Sep 17 00:00:00 2001 From: Jens Luetjen Date: Wed, 17 Feb 2016 23:41:56 +0100 Subject: [PATCH 16/46] Avoid endless loop in 'next' and 'previous'. --- mopidy/core/playback.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 0d945b1c..ab96171e 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -295,6 +295,9 @@ class PlaybackController(object): """ state = self.get_state() current = self._pending_tl_track or self._current_tl_track + # avoid endless loop if 'repeat' is 'true' and no track is playable + # * 2 -> second run to get all playable track in a shuffled playlist + count = self.core.tracklist.get_length() * 2 while current: pending = self.core.tracklist.next_track(current) @@ -306,6 +309,10 @@ class PlaybackController(object): # if current == pending: # break current = pending + count -= 1 + if not count: + logger.info('No playable track in the list.') + break # TODO return result? @@ -428,6 +435,9 @@ class PlaybackController(object): self._previous = True state = self.get_state() current = self._pending_tl_track or self._current_tl_track + # avoid endless loop if 'repeat' is 'true' and no track is playable + # * 2 -> second run to get all playable track in a shuffled playlist + count = self.core.tracklist.get_length() * 2 while current: pending = self.core.tracklist.previous_track(current) @@ -439,6 +449,10 @@ class PlaybackController(object): # if current == pending: # break current = pending + count -= 1 + if not count: + logger.info('No playable track in the list.') + break # TODO: no return value? From bf4da7a62700d623757a6e9de94b28cc2ed6c7cc Mon Sep 17 00:00:00 2001 From: Jens Luetjen Date: Thu, 18 Feb 2016 20:30:28 +0100 Subject: [PATCH 17/46] Add tests to ensure that play, next will not busy-loop Test PlaybackController functions play(), next(), previous() and _on_about_to_finish() that they will not loop forever if all tracks are unplayable. --- tests/core/test_playback.py | 105 ++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index cfd58793..3572800c 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -14,11 +14,42 @@ from tests import dummy_audio class TestPlaybackProvider(backend.PlaybackProvider): + + def __init__(self, audio, backend): + super(TestPlaybackProvider, self).__init__(audio, backend) + self._call_limit = 10 + self._call_count = 0 + self._call_onetime = False + + def reset_call_limit(self): + self._call_count = 0 + self._call_onetime = False + + def is_call_limit_reached(self): + return self._call_count > self._call_limit + + def _translate_uri_call_limit(self, uri): + self._call_count += 1 + if self._call_count > self._call_limit: + # return any url (not 'None') to stop the endless loop + return 'assert: call limit reached' + if 'limit_never' in uri: + # unplayable + return None + elif 'limit_one' in uri: + # one time playable + if self._call_onetime: + return None + self._call_onetime = True + return uri + def translate_uri(self, uri): if 'error' in uri: raise Exception(uri) elif 'unplayable' in uri: return None + elif 'limit' in uri: + return self._translate_uri_call_limit(uri) else: return uri @@ -1125,3 +1156,77 @@ class TestBug1352Regression(BaseTest): self.core.history._add_track.assert_called_once_with(self.tracks[1]) self.core.tracklist._mark_playing.assert_called_once_with(tl_tracks[1]) + + +class TestEndlessLoop(BaseTest): + + tracks_play = [ + Track(uri='dummy:limit_never:a'), + Track(uri='dummy:limit_never:b') + ] + + tracks_other = [ + Track(uri='dummy:limit_never:a'), + Track(uri='dummy:limit_one'), + Track(uri='dummy:limit_never:b') + ] + + def test_play(self): + self.core.tracklist.clear() + self.core.tracklist.add(self.tracks_play) + + self.backend.playback.reset_call_limit().get() + self.core.tracklist.set_repeat(True) + + tl_tracks = self.core.tracklist.get_tl_tracks() + self.core.playback.play(tl_tracks[0]) + self.replay_events() + + self.assertFalse(self.backend.playback.is_call_limit_reached().get()) + + def test_next(self): + self.core.tracklist.clear() + self.core.tracklist.add(self.tracks_other) + + self.backend.playback.reset_call_limit().get() + self.core.tracklist.set_repeat(True) + + tl_tracks = self.core.tracklist.get_tl_tracks() + self.core.playback.play(tl_tracks[1]) + self.replay_events() + + self.core.playback.next() + self.replay_events() + + self.assertFalse(self.backend.playback.is_call_limit_reached().get()) + + def test_previous(self): + self.core.tracklist.clear() + self.core.tracklist.add(self.tracks_other) + + self.backend.playback.reset_call_limit().get() + self.core.tracklist.set_repeat(True) + + tl_tracks = self.core.tracklist.get_tl_tracks() + self.core.playback.play(tl_tracks[1]) + self.replay_events() + + self.core.playback.previous() + self.replay_events() + + self.assertFalse(self.backend.playback.is_call_limit_reached().get()) + + def test_on_about_to_finish(self): + self.core.tracklist.clear() + self.core.tracklist.add(self.tracks_other) + + self.backend.playback.reset_call_limit().get() + self.core.tracklist.set_repeat(True) + + tl_tracks = self.core.tracklist.get_tl_tracks() + self.core.playback.play(tl_tracks[1]) + self.replay_events() + + self.trigger_about_to_finish() + + self.assertFalse(self.backend.playback.is_call_limit_reached().get()) From aafcf8559d0a56db9f9a3d384535c6ba1272f20e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 26 Mar 2016 09:51:14 +0100 Subject: [PATCH 18/46] docs: Add PR #1455 to changelog --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2e9ca56a..ee2ddf8c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,9 @@ Bug fix release. (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480` PR: :issue:`1487`) +- Core: Avoid endless loop if all tracks in the tracklist are unplayable and + consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) + v2.0.0 (2016-02-15) =================== From 8820a88e0c36e2f813c39c448428a4800019d9d6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 26 Mar 2016 11:27:57 +0100 Subject: [PATCH 19/46] file: Fix crash when media_dirs contains non-ASCII chars Fixes #1345 --- docs/changelog.rst | 4 ++++ mopidy/file/library.py | 3 ++- mopidy/internal/path.py | 5 +++++ tests/internal/test_path.py | 26 ++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ee2ddf8c..067e59fb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,6 +26,10 @@ Bug fix release. - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) +- File: Ensure path comparision is done between bytestrings only. Fixes crash + where a :confval:`file/media_dirs` path contained non-ASCII characters. + (Fixes: :issue:`1345`, PR: :issue:`1493`) + v2.0.0 (2016-02-15) =================== diff --git a/mopidy/file/library.py b/mopidy/file/library.py index 09fa2cf1..10182a38 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -132,5 +132,6 @@ class FileLibraryProvider(backend.LibraryProvider): def _is_in_basedir(self, local_path): return any( - path.is_path_inside_base_dir(local_path, media_dir['path']) + path.is_path_inside_base_dir( + local_path, media_dir['path'].encode('utf-8')) for media_dir in self._media_dirs) diff --git a/mopidy/internal/path.py b/mopidy/internal/path.py index 498b3016..1c10736f 100644 --- a/mopidy/internal/path.py +++ b/mopidy/internal/path.py @@ -196,6 +196,11 @@ def find_mtimes(root, follow=False): def is_path_inside_base_dir(path, base_path): + if not isinstance(path, bytes): + raise ValueError('path is not a bytestring') + if not isinstance(base_path, bytes): + raise ValueError('base_path is not a bytestring') + if path.endswith(os.sep): raise ValueError('Path %s cannot end with a path separator' % path) diff --git a/tests/internal/test_path.py b/tests/internal/test_path.py index 9e09c39a..6eebaaa3 100644 --- a/tests/internal/test_path.py +++ b/tests/internal/test_path.py @@ -7,6 +7,8 @@ import shutil import tempfile import unittest +import pytest + from mopidy import compat, exceptions from mopidy.internal import path from mopidy.internal.gi import GLib @@ -392,6 +394,30 @@ class FindMTimesTest(unittest.TestCase): self.assertEqual(errors, {}) +class TestIsPathInsideBaseDir(object): + def test_when_inside(self): + assert path.is_path_inside_base_dir( + '/æ/øå'.encode('utf-8'), + '/æ'.encode('utf-8')) + + def test_when_outside(self): + assert not path.is_path_inside_base_dir( + '/æ/øå'.encode('utf-8'), + '/ø'.encode('utf-8')) + + def test_byte_inside_str_fails(self): + with pytest.raises(ValueError): + path.is_path_inside_base_dir('/æ/øå'.encode('utf-8'), '/æ') + + def test_str_inside_byte_fails(self): + with pytest.raises(ValueError): + path.is_path_inside_base_dir('/æ/øå', '/æ'.encode('utf-8')) + + def test_str_inside_str_fails(self): + with pytest.raises(ValueError): + path.is_path_inside_base_dir('/æ/øå', '/æ') + + # TODO: kill this in favour of just os.path.getmtime + mocks class MtimeTest(unittest.TestCase): From 095329ce8224cf19f8c652d9d9abdd2b447357de Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 26 Mar 2016 11:55:11 +0100 Subject: [PATCH 20/46] docs: PR #1472 fixes #1469 --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ee2ddf8c..acb8f0c7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,12 +16,12 @@ Bug fix release. warning. - Audio: Fix buffer conversion. This fixes image extraction. - (PR: :issue:`1472`) + (Fixes: :issue:`1469`, PR: :issue:`1472`) - Audio: Update scan logic to workaround GStreamer issue where tags and duration might only be available after we start playing. - (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480` - PR: :issue:`1487`) + (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480`, PR: + :issue:`1487`) - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) From 4bbd847a6a508b5bba7668e3847f2e3c721401c5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 26 Mar 2016 21:27:03 +0100 Subject: [PATCH 21/46] audio: Always install audio element --- mopidy/audio/actor.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 267b228d..1fb4fec6 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -22,7 +22,6 @@ logger = logging.getLogger(__name__) gst_logger = logging.getLogger('mopidy.audio.gst') _GST_PLAY_FLAGS_AUDIO = 0x02 -_GST_PLAY_FLAGS_SOFT_VOLUME = 0x10 _GST_STATE_MAPPING = { Gst.State.PLAYING: PlaybackState.PLAYING, @@ -451,8 +450,7 @@ class Audio(pykka.ThreadingActor): def _setup_playbin(self): playbin = Gst.ElementFactory.make('playbin') - playbin.set_property( - 'flags', _GST_PLAY_FLAGS_AUDIO | _GST_PLAY_FLAGS_SOFT_VOLUME) + playbin.set_property('flags', _GST_PLAY_FLAGS_AUDIO) # TODO: turn into config values... playbin.set_property('buffer-size', 5 << 20) # 5MB @@ -489,15 +487,16 @@ class Audio(pykka.ThreadingActor): def _setup_audio_sink(self): audio_sink = Gst.ElementFactory.make('bin', 'audio-sink') + queue = Gst.ElementFactory.make('queue') + volume = Gst.ElementFactory.make('volume') # Queue element to buy us time between the about-to-finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. + # TODO: See if settings should be set to minimize latency. Previous # setting breaks appsrc, and settings before that broke on a few # 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', @@ -505,15 +504,13 @@ class Audio(pykka.ThreadingActor): audio_sink.add(queue) audio_sink.add(self._outputs) + audio_sink.add(volume) + + queue.link(volume) + volume.link(self._outputs) if self.mixer: - volume = Gst.ElementFactory.make('volume') - audio_sink.add(volume) - queue.link(volume) - volume.link(self._outputs) self.mixer.setup(volume, self.actor_ref.proxy().mixer) - else: - queue.link(self._outputs) ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink')) audio_sink.add_pad(ghost_pad) From fb823d1a7c396ec6b0bda4070c92cb0b7b3e1ac3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2016 21:09:38 +0200 Subject: [PATCH 22/46] lint: Workaround and fix to account for new version of flake8 --- mopidy/__main__.py | 2 +- mopidy/config/__init__.py | 7 +++++-- tests/models/test_fields.py | 2 +- tox.ini | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 86a0c19c..7963900e 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -5,7 +5,7 @@ import os import signal import sys -from mopidy.internal.gi import Gst # noqa: Import to initialize +from mopidy.internal.gi import Gst # noqa: F401 try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py index 21a6a00b..ec5c9a99 100644 --- a/mopidy/config/__init__.py +++ b/mopidy/config/__init__.py @@ -9,12 +9,15 @@ import re from mopidy import compat from mopidy.compat import configparser from mopidy.config import keyring -from mopidy.config.schemas import * # noqa -from mopidy.config.types import * # noqa +from mopidy.config.schemas import * +from mopidy.config.types import * from mopidy.internal import path, versioning logger = logging.getLogger(__name__) +# flake8: noqa: +# TODO: Update this to be flake8 compliant + _core_schema = ConfigSchema('core') _core_schema['cache_dir'] = Path() _core_schema['config_dir'] = Path() diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py index bf842fd5..3374c822 100644 --- a/tests/models/test_fields.py +++ b/tests/models/test_fields.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import unittest -from mopidy.models.fields import * # noqa: F403 +from mopidy.models.fields import Collection, Field, Integer, String def create_instance(field): diff --git a/tox.ini b/tox.ini index da6bcc38..b39fc68b 100644 --- a/tox.ini +++ b/tox.ini @@ -37,7 +37,8 @@ commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:flake8] deps = flake8 - flake8-import-order +# TODO: Re-enable once https://github.com/PyCQA/flake8-import-order/issues/79 is released. +# flake8-import-order pep8-naming commands = flake8 --show-source --statistics mopidy tests From 3ee91240a0c4bb4403ef1c636a12cd96300b40d7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 13 Jun 2016 22:44:03 +0200 Subject: [PATCH 23/46] Merge pull request #1496 from dublok/fix/1462-flac-seek-freeze audio: Ignore position of _on_position_changed callback (fixes #1462) --- mopidy/core/playback.py | 4 ++-- tests/core/test_playback.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index ab96171e..da505b22 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -230,8 +230,8 @@ class PlaybackController(object): self._seek(self._pending_position) def _on_position_changed(self, position): - if self._pending_position == position: - self._trigger_seeked(position) + if self._pending_position is not None: + self._trigger_seeked(self._pending_position) self._pending_position = None def _on_about_to_finish_callback(self): diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py index 3572800c..34c9d367 100644 --- a/tests/core/test_playback.py +++ b/tests/core/test_playback.py @@ -734,6 +734,7 @@ class EventEmissionTest(BaseTest): self.core.playback.play(tl_tracks[0]) self.trigger_about_to_finish(replay_until='stream_changed') + self.replay_events() listener_mock.reset_mock() self.core.playback.seek(1000) From 5e2b44ab44afd2e4db0387fae5e36578df7ef6d9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 13 Jun 2016 22:12:56 +0200 Subject: [PATCH 24/46] Merge pull request #1522 from SeppSTA/fix/1521-download-timeout-sec fix/1521 download timeout sec --- mopidy/stream/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 0861b5b0..1bdd05ca 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -68,7 +68,7 @@ class StreamLibraryProvider(backend.LibraryProvider): 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) + logger.warning('Problem looking up %s', uri) track = Track(uri=uri) return [track] @@ -142,7 +142,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session): uri, timeout) return None, None content = http.download( - requests_session, uri, timeout=download_timeout) + requests_session, uri, timeout=download_timeout / 1000) if content is None: logger.info( From 0c6be281dfd01df80f0d4a95f925dace19b31f7c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2016 21:47:10 +0200 Subject: [PATCH 25/46] doc: Add changelog entry for PR#1496 and PR#1522 --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8830edd3..60255266 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,9 @@ Bug fix release. (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480`, PR: :issue:`1487`) +- Audio: Better handling of seek when position does not match the expected + pending position. (Fixes: :issue:`1462` PR: :issue:`1496`) + - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) @@ -30,6 +33,9 @@ Bug fix release. where a :confval:`file/media_dirs` path contained non-ASCII characters. (Fixes: :issue:`1345`, PR: :issue:`1493`) +- Stream: Fix milliseconds vs seconds mistake in timeout handling. + (Fixes: :issue:`1521` PR: :issue:`1522`) + v2.0.0 (2016-02-15) =================== From 692138a51e146d1b25a8d0db4ad057773406c6c5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2016 22:00:51 +0200 Subject: [PATCH 26/46] Merge pull request #1525 from palfrey/dodgy-date-tags If the date in a tag is invalid, skip it --- mopidy/audio/tags.py | 13 +++++++++---- tests/audio/test_tags.py | 8 ++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 5ae86468..7fabefd6 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -44,10 +44,15 @@ gstreamer-GstTagList.html value = taglist.get_value_index(tag, i) if isinstance(value, GLib.Date): - date = datetime.date( - value.get_year(), value.get_month(), value.get_day()) - result[tag].append(date.isoformat().decode('utf-8')) - if isinstance(value, Gst.DateTime): + try: + date = datetime.date( + value.get_year(), value.get_month(), value.get_day()) + result[tag].append(date.isoformat().decode('utf-8')) + except ValueError: + logger.debug( + 'Ignoring dodgy date value: %d-%d-%d', + value.get_year(), value.get_month(), value.get_day()) + elif isinstance(value, Gst.DateTime): result[tag].append(value.to_iso8601_string().decode('utf-8')) elif isinstance(value, bytes): result[tag].append(value.decode('utf-8', 'replace')) diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 01475124..d85bcc12 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -44,6 +44,14 @@ class TestConvertTaglist(object): assert isinstance(result[Gst.TAG_DATE][0], compat.text_type) assert result[Gst.TAG_DATE][0] == '2014-01-07' + def test_date_tag_bad_value(self): + date = GLib.Date.new_dmy(7, 1, 10000) + taglist = self.make_taglist(Gst.TAG_DATE, [date]) + + result = tags.convert_taglist(taglist) + + assert len(result[Gst.TAG_DATE]) == 0 + def test_date_time_tag(self): taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12') From 44ff6697444574da3ad8035c1dc5eaaa4b34bfd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2016 22:05:34 +0200 Subject: [PATCH 27/46] Merge pull request #1534 from edran/fix-scrobbling Get correct track position on change events --- docs/changelog.rst | 4 ++++ mopidy/core/playback.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 60255266..8ed0bbb9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -29,6 +29,10 @@ Bug fix release. - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) +- Core: Correctly record the last position of a track when switching to another + one. Particularly relevant for `mopidy-scrobbler` users, as before it was + essentially unusable. (Fixes: :issue:`1456`, PR: :issue:`1534`) + - File: Ensure path comparision is done between bytestrings only. Fixes crash where a :confval:`file/media_dirs` path contained non-ASCII characters. (Fixes: :issue:`1345`, PR: :issue:`1493`) diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index da505b22..0106abf2 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -251,6 +251,12 @@ class PlaybackController(object): if self._state == PlaybackState.STOPPED: return + # Unless overridden by other calls (e.g. next / previous / stop) this + # will be the last position recorded until the track gets reassigned. + # TODO: Check if case when track.length isn't populated needs to be + # handled. + self._last_position = self._current_tl_track.track.length + pending = self.core.tracklist.eot_track(self._current_tl_track) # avoid endless loop if 'repeat' is 'true' and no track is playable # * 2 -> second run to get all playable track in a shuffled playlist @@ -394,6 +400,10 @@ class PlaybackController(object): if not backend: return False + # This must happen before prepare_change gets called, otherwise the + # backend flushes the information of the track. + self._last_position = self.get_time_position() + # TODO: Wrap backend call in error handling. backend.playback.prepare_change() From c1679964ff2a5062ddc637cb3add9d3938cc05b1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2016 22:09:52 +0200 Subject: [PATCH 28/46] docs: Add PR#1525 and PR#1517 to changelog --- docs/changelog.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8ed0bbb9..e63204c5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,7 +24,11 @@ Bug fix release. :issue:`1487`) - Audio: Better handling of seek when position does not match the expected - pending position. (Fixes: :issue:`1462` PR: :issue:`1496`) + pending position. (Fixes: :issue:`1462`, PR: :issue:`1496`) + +- Audio: Handle bad date tags from audio, thanks to Mario Lang and Tom Parker + who fixed this in parallel. (Fixes: :issue:`1506`, PR: :issue:`1525`, + :issue:`1517`) - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) @@ -38,7 +42,7 @@ Bug fix release. (Fixes: :issue:`1345`, PR: :issue:`1493`) - Stream: Fix milliseconds vs seconds mistake in timeout handling. - (Fixes: :issue:`1521` PR: :issue:`1522`) + (Fixes: :issue:`1521`, PR: :issue:`1522`) v2.0.0 (2016-02-15) From 242a771062ab4da277096cdd94f73e1aab118ced Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2016 23:48:53 +0200 Subject: [PATCH 29/46] docs: Add sphinx_rtd_theme in docs/requirements.txt --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index c75793d9..62c7e3e5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ Sphinx >= 1.0 +sphinx_rtd_theme pygraphviz From ad4225d38d717d1c1e88e4ca517419290995c914 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Jul 2016 21:16:31 +0200 Subject: [PATCH 30/46] audio: Make scanner handle media with not duration (Fixes: #1526) --- docs/changelog.rst | 3 +++ mopidy/audio/scan.py | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e63204c5..c69514da 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,6 +30,9 @@ Bug fix release. who fixed this in parallel. (Fixes: :issue:`1506`, PR: :issue:`1525`, :issue:`1517`) +- Audio: Make sure scanner handles streams without a duration. + (Fixes: :issue:`1526`) + - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 27888638..f99c4489 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -135,6 +135,17 @@ def _start_pipeline(pipeline): pipeline.set_state(Gst.State.PLAYING) +def _query_duration(pipeline): + success, duration = pipeline.query_duration(Gst.Format.TIME) + if not success: + duration = None # Make sure error case preserves None. + elif duration < 0: + duration = None # Stream without duration. + else: + duration = duration // Gst.MSECOND + return success, duration + + def _query_seekable(pipeline): query = Gst.Query.new_seeking(Gst.Format.TIME) pipeline.query(query) @@ -187,13 +198,8 @@ def _process(pipeline, timeout_ms): elif message.type == Gst.MessageType.EOS: return tags, mime, have_audio, duration elif message.type == Gst.MessageType.ASYNC_DONE: - success, duration = pipeline.query_duration(Gst.Format.TIME) - if success: - duration = duration // Gst.MSECOND - else: - duration = None - - if tags and duration is not None: + success, duration = _query_duration(pipeline) + if tags and success: return tags, mime, have_audio, duration # Workaround for upstream bug which causes tags/duration to arrive From e594d560ff66b9795fb744e64d3ddd378d37b148 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Jul 2016 21:33:16 +0200 Subject: [PATCH 31/46] audio: Make sure tags are never none (Fixes #1449) --- docs/changelog.rst | 2 ++ mopidy/audio/actor.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c69514da..e82b2d06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,6 +33,8 @@ Bug fix release. - Audio: Make sure scanner handles streams without a duration. (Fixes: :issue:`1526`) +- Audio: Ensure audio tags are never `None`. (Fixes: :issue:`1449`) + - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 1fb4fec6..61a8e008 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -368,7 +368,7 @@ class _Handler(object): # 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 + self._audio._tags = tags or {} if tags: logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) From 8707a7c9cf1d581911b76b2379fab8d014044e3b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 26 Jul 2016 21:41:49 +0200 Subject: [PATCH 32/46] lint: Re-enable flake8-import-order --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b39fc68b..da6bcc38 100644 --- a/tox.ini +++ b/tox.ini @@ -37,8 +37,7 @@ commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv:flake8] deps = flake8 -# TODO: Re-enable once https://github.com/PyCQA/flake8-import-order/issues/79 is released. -# flake8-import-order + flake8-import-order pep8-naming commands = flake8 --show-source --statistics mopidy tests From 02a2788da0e52d73675d9fa665ebb70747e9e381 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 08:37:44 +0200 Subject: [PATCH 33/46] docs: Cleanup changelog --- docs/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e82b2d06..4dfa761d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,7 +20,7 @@ Bug fix release. - Audio: Update scan logic to workaround GStreamer issue where tags and duration might only be available after we start playing. - (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480`, PR: + (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474`, :issue:`1480`, PR: :issue:`1487`) - Audio: Better handling of seek when position does not match the expected @@ -33,13 +33,13 @@ Bug fix release. - Audio: Make sure scanner handles streams without a duration. (Fixes: :issue:`1526`) -- Audio: Ensure audio tags are never `None`. (Fixes: :issue:`1449`) +- Audio: Ensure audio tags are never ``None``. (Fixes: :issue:`1449`) - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) - Core: Correctly record the last position of a track when switching to another - one. Particularly relevant for `mopidy-scrobbler` users, as before it was + one. Particularly relevant for Mopidy-Scrobbler users, as before it was essentially unusable. (Fixes: :issue:`1456`, PR: :issue:`1534`) - File: Ensure path comparision is done between bytestrings only. Fixes crash From 4ec06ef8f969f1a4aa17e40020c403aa87a494cc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 08:37:55 +0200 Subject: [PATCH 34/46] docs: Remove broken download stats --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index 88072eb5..6720e9e8 100644 --- a/README.rst +++ b/README.rst @@ -61,10 +61,6 @@ To get started with Mopidy, check out :target: https://pypi.python.org/pypi/Mopidy/ :alt: Latest PyPI version -.. image:: https://img.shields.io/pypi/dm/Mopidy.svg?style=flat - :target: https://pypi.python.org/pypi/Mopidy/ - :alt: Number of PyPI downloads - .. image:: https://img.shields.io/travis/mopidy/mopidy/develop.svg?style=flat :target: https://travis-ci.org/mopidy/mopidy :alt: Travis CI build status From 12e0c87c99f56a03d5af764023aada70a7edbb7b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 08:51:20 +0200 Subject: [PATCH 35/46] docs: Fix warnings about unknown options --- docs/changelog.rst | 57 +++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4dfa761d..7f0e5508 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2091,10 +2091,10 @@ A release with a number of small and medium fixes, with no specific focus. - Converted from the optparse to the argparse library for handling command line options. -- :option:`mopidy --show-config` will now take into consideration any +- ``mopidy --show-config`` will now take into consideration any :option:`mopidy --option` arguments appearing later on the command line. This helps you see the effective configuration for runs with the same - :option:`mopidy --options` arguments. + ``mopidy --options`` arguments. **Audio** @@ -2168,7 +2168,7 @@ v0.14.1 (2013-04-28) ==================== This release addresses an issue in v0.14.0 where the new -:option:`mopidy-convert-config` tool and the new :option:`mopidy --option` +``mopidy-convert-config`` tool and the new :option:`mopidy --option` command line option was broken because some string operations inadvertently converted some byte strings to unicode. @@ -2198,7 +2198,7 @@ one new. As part of this change we have cleaned up the naming of our config values. - To ease migration we've made a tool named :option:`mopidy-convert-config` for + To ease migration we've made a tool named ``mopidy-convert-config`` for automatically converting the old ``settings.py`` to a new ``mopidy.conf`` file. This tool takes care of all the renamed config values as well. See ``mopidy-convert-config`` for details on how to use it. @@ -2231,11 +2231,11 @@ one new. **Command line options** -- The command option :option:`mopidy --list-settings` is now named - :option:`mopidy --show-config`. +- The command option ``mopidy --list-settings`` is now named + ``mopidy --show-config``. -- The command option :option:`mopidy --list-deps` is now named - :option:`mopidy --show-deps`. +- The command option ``mopidy --list-deps`` is now named + ``mopidy --show-deps``. - What configuration files to use can now be specified through the command option :option:`mopidy --config`, multiple files can be specified using colon @@ -2245,8 +2245,8 @@ one new. :option:`mopidy --option`. For example: ``mopidy --option spotify/enabled=false``. -- The GStreamer command line options, :option:`mopidy --gst-*` and - :option:`mopidy --help-gst` are no longer supported. To set GStreamer debug +- The GStreamer command line options, ``mopidy --gst-*`` and + ``mopidy --help-gst`` are no longer supported. To set GStreamer debug flags, you can use environment variables such as :envvar:`GST_DEBUG`. Refer to GStreamer's documentation for details. @@ -2295,7 +2295,7 @@ already have. **Core** - Removed the :attr:`mopidy.settings.DEBUG_THREAD` setting and the - :option:`--debug-thread` command line option. Sending SIGUSR1 to + ``mopidy --debug-thread`` command line option. Sending SIGUSR1 to the Mopidy process will now always make it log tracebacks for all alive threads. @@ -2576,9 +2576,8 @@ We've added an HTTP frontend for those wanting to build web clients for Mopidy! - Make ``mopidy-scan`` ignore invalid dates, e.g. dates in years outside the range 1-9999. -- Make ``mopidy-scan`` accept :option:`-q`/:option:`--quiet` and - :option:`-v`/:option:`--verbose` options to control the amount of logging - output when scanning. +- Make ``mopidy-scan`` accept ``-q``/``--quiet`` and ``-v``/``--verbose`` + options to control the amount of logging output when scanning. - The scanner can now handle files with other encodings than UTF-8. Rebuild your tag cache with ``mopidy-scan`` to include tracks that may have been @@ -2703,7 +2702,7 @@ long time been our most requested feature. Finally, it's here! **Developer support** - Added optional background thread for debugging deadlocks. When the feature is - enabled via the ``--debug-thread`` option or + enabled via the ``mopidy --debug-thread`` option or :attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump the traceback for all running threads. @@ -2915,9 +2914,9 @@ resolved a bunch of related issues. known setting, and suggests to the user what we think the setting should have been. -- Added :option:`--list-deps` option to the ``mopidy`` command that lists - required and optional dependencies, their current versions, and some other - information useful for debugging. (Fixes: :issue:`74`) +- Added ``mopidy --list-deps`` option that lists required and optional + dependencies, their current versions, and some other information useful for + debugging. (Fixes: :issue:`74`) - Added ``tools/debug-proxy.py`` to tee client requests to two backends and diff responses. Intended as a developer tool for checking for MPD protocol @@ -3197,12 +3196,12 @@ Please note that 0.5.0 requires some updated dependencies, as listed under - Command line usage: - - Support passing options to GStreamer. See :option:`--help-gst` for a list + - Support passing options to GStreamer. See ``mopidy --help-gst`` for a list of available options. (Fixes: :issue:`95`) - - Improve :option:`--list-settings` output. (Fixes: :issue:`91`) + - Improve ``mopidy --list-settings`` output. (Fixes: :issue:`91`) - - Added :option:`--interactive` for reading missing local settings from + - Added ``mopidy --interactive`` for reading missing local settings from ``stdin``. (Fixes: :issue:`96`) - Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``, @@ -3345,8 +3344,8 @@ loading from Mopidy 0.3.0 is still present. - Settings: - - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins - Grunskis for the bug report and patch. (Fixes: :issue:`63`) + - Fix crash on ``mopidy --list-settings`` on clean installation. Thanks to + Martins Grunskis for the bug report and patch. (Fixes: :issue:`63`) - Packaging: @@ -3574,11 +3573,11 @@ to Valentin David. - Simplify the default log format, :attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view: Less noise, more information. - - Rename the :option:`--dump` command line option to - :option:`--save-debug-log`. + - Rename the ``mopidy --dump`` command line option to + :option:`mopidy --save-debug-log`. - Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to - :attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose` - too. + :attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for + :option:`mopidy --verbose` too. - Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to :attr:`mopidy.settings.DEBUG_LOG_FILENAME`. @@ -3644,7 +3643,7 @@ fixing the OS X issues for a future release. You can track the progress at - Exit early if not Python >= 2.6, < 3. - Validate settings at startup and print useful error messages if the settings has not been updated or anything is misspelled. -- Add command line option :option:`--list-settings` to print the currently +- Add command line option ``mopidy --list-settings`` to print the currently active settings. - Include Sphinx scripts for building docs, pylintrc, tests and test data in the packages created by ``setup.py`` for i.e. PyPI. @@ -3826,7 +3825,7 @@ the established pace of at least a release per month. - Improvements to MPD protocol handling, making Mopidy work much better with a group of clients, including ncmpc, MPoD, and Theremin. -- New command line flag :option:`--dump` for dumping debug log to ``dump.log`` +- New command line flag ``mopidy --dump`` for dumping debug log to ``dump.log`` in the current directory. - New setting :attr:`mopidy.settings.MIXER_ALSA_CONTROL` for forcing what ALSA control :class:`mopidy.mixers.alsa.AlsaMixer` should use. From 0d16502da130c9311b6523e316849ac8d0fc2764 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 08:58:13 +0200 Subject: [PATCH 36/46] docs: Require Pykka for docs building (fixes #1500) Pykka is a pure Python dependency, so we might as well depend on it for docs building so that :autoclass: works on Pykka subclasses in the docs. --- docs/changelog.rst | 5 +++++ docs/conf.py | 1 - docs/requirements.txt | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7f0e5508..22199087 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -49,6 +49,11 @@ Bug fix release. - Stream: Fix milliseconds vs seconds mistake in timeout handling. (Fixes: :issue:`1521`, PR: :issue:`1522`) +- Docs: Fix the rendering of :class:`mopidy.core.Core` and + :class:`mopidy.audio.Audio` docs. This should also contribute towards making + the Mopidy Debian package build bit-by-bit reproducible. (Fixes: + :issue:`1500`) + v2.0.0 (2016-02-15) =================== diff --git a/docs/conf.py b/docs/conf.py index 208822a2..cb04a671 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,7 +40,6 @@ MOCK_MODULES = [ 'dbus.mainloop.glib', 'dbus.service', 'mopidy.internal.gi', - 'pykka', ] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() diff --git a/docs/requirements.txt b/docs/requirements.txt index 62c7e3e5..f0cc5e6c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ Sphinx >= 1.0 -sphinx_rtd_theme pygraphviz +Pykka >= 1.1 +sphinx_rtd_theme From 3e12ed1f69d63bbab57d0bcde1ff9f6a7d2674d6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2016 21:16:21 +0200 Subject: [PATCH 37/46] audio: Postpone set_metadata until track is playing --- docs/changelog.rst | 3 +++ mopidy/audio/actor.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 22199087..a13baa3d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,9 @@ Bug fix release. - Audio: Ensure audio tags are never ``None``. (Fixes: :issue:`1449`) +- Audio: Update :meth:`mopidy.audio.Audio.set_metadata` to postpone sending + tags if there is a pending track change. (Fixes: :issue:`1357`) + - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 61a8e008..f96834e7 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -374,6 +374,10 @@ class _Handler(object): logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) AudioListener.send('tags_changed', tags=tags.keys()) + if self._audio._pending_metadata: + self._audio._playbin.send_event(self._audio._pending_metadata) + self._audio._pending_metadata = None + def on_segment(self, segment): gst_logger.debug( 'Got SEGMENT pad event: ' @@ -412,6 +416,7 @@ class Audio(pykka.ThreadingActor): self._tags = {} self._pending_uri = None self._pending_tags = None + self._pending_metadata = None self._playbin = None self._outputs = None @@ -800,12 +805,11 @@ class Audio(pykka.ThreadingActor): if track.album and track.album.name: set_value(Gst.TAG_ALBUM, track.album.name) - gst_logger.debug( - 'Sending TAG event for track %r: %r', - track.uri, taglist.to_string()) event = Gst.Event.new_tag(taglist) - # TODO: check if we get this back on our own bus? - self._playbin.send_event(event) + if not self._pending_uri: + self._playbin.send_event(event) + else: + self._pending_metadata = event def get_current_tags(self): """ From 5e8682cc51d751bef70c7e7a795f91ac9be9ed24 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 09:24:45 +0200 Subject: [PATCH 38/46] audio: Address my own comments on PR#1538 --- docs/changelog.rst | 3 ++- mopidy/audio/actor.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a13baa3d..84bac249 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -36,7 +36,8 @@ Bug fix release. - Audio: Ensure audio tags are never ``None``. (Fixes: :issue:`1449`) - Audio: Update :meth:`mopidy.audio.Audio.set_metadata` to postpone sending - tags if there is a pending track change. (Fixes: :issue:`1357`) + tags if there is a pending track change. (Fixes: :issue:`1357`, PR: + :issue:`1538`) - Core: Avoid endless loop if all tracks in the tracklist are unplayable and consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index f96834e7..6020bc1b 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -805,11 +805,14 @@ class Audio(pykka.ThreadingActor): if track.album and track.album.name: set_value(Gst.TAG_ALBUM, track.album.name) + gst_logger.debug( + 'Sending TAG event for track %r: %r', + track.uri, taglist.to_string()) event = Gst.Event.new_tag(taglist) - if not self._pending_uri: - self._playbin.send_event(event) - else: + if self._pending_uri: self._pending_metadata = event + else: + self._playbin.send_event(event) def get_current_tags(self): """ From a3164ca43b29fc67cf3dfe6b4eba79bbea58ae46 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 09:30:46 +0200 Subject: [PATCH 39/46] docs: PR#1496 fixed #1505 --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 84bac249..d6548865 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,7 +24,7 @@ Bug fix release. :issue:`1487`) - Audio: Better handling of seek when position does not match the expected - pending position. (Fixes: :issue:`1462`, PR: :issue:`1496`) + pending position. (Fixes: :issue:`1462`, :issue:`1505`, PR: :issue:`1496`) - Audio: Handle bad date tags from audio, thanks to Mario Lang and Tom Parker who fixed this in parallel. (Fixes: :issue:`1506`, PR: :issue:`1525`, From 6a01a2e90618a78201175f09f4b5a12841bea409 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 21:25:46 +0200 Subject: [PATCH 40/46] models: Test Identifier field type --- tests/models/test_fields.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py index 3374c822..69f4cd91 100644 --- a/tests/models/test_fields.py +++ b/tests/models/test_fields.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals import unittest -from mopidy.models.fields import Collection, Field, Integer, String +from mopidy.models.fields import Collection, Field, Identifier, Integer, String def create_instance(field): @@ -126,6 +126,37 @@ class StringTest(unittest.TestCase): self.assertEqual('', instance.attr) +class IdentifierTest(unittest.TestCase): + def test_default_handling(self): + instance = create_instance(Identifier(default='abc')) + self.assertEqual('abc', instance.attr) + + def test_native_str_allowed(self): + instance = create_instance(Identifier()) + instance.attr = str('abc') + self.assertEqual('abc', instance.attr) + + def test_bytes_allowed(self): + instance = create_instance(Identifier()) + instance.attr = b'abc' + self.assertEqual(b'abc', instance.attr) + + def test_unicode_allowed(self): + instance = create_instance(Identifier()) + instance.attr = u'abc' + self.assertEqual(u'abc', instance.attr) + + def test_other_disallowed(self): + instance = create_instance(Identifier()) + with self.assertRaises(TypeError): + instance.attr = 1234 + + def test_empty_string(self): + instance = create_instance(Identifier()) + instance.attr = '' + self.assertEqual('', instance.attr) + + class IntegerTest(unittest.TestCase): def test_default_handling(self): instance = create_instance(Integer(default=1234)) From 2d4439a03a16766ceaabd0e3deb4e1c07b6b4803 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 21:25:36 +0200 Subject: [PATCH 41/46] audio: Formatting --- mopidy/audio/tags.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 7fabefd6..e4d86dc7 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -136,12 +136,11 @@ def convert_tags_to_track(tags): return Track(**track_kwargs) -def _artists( - tags, artist_name, artist_id=None, artist_sortname=None): - +def _artists(tags, artist_name, artist_id=None, artist_sortname=None): # Name missing, don't set artist if not tags.get(artist_name): return None + # One artist name and either id or sortname, include all available fields if len(tags[artist_name]) == 1 and \ (artist_id in tags or artist_sortname in tags): From 4e91f289448b99df62af5b1a890d04d968e3eb02 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 9 Aug 2016 22:37:44 +0200 Subject: [PATCH 42/46] docs: Remove GlobalSign as cert has expired --- docs/sponsors.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/sponsors.rst b/docs/sponsors.rst index 2d8b7f4e..2528247b 100644 --- a/docs/sponsors.rst +++ b/docs/sponsors.rst @@ -31,10 +31,3 @@ accelerate requests to all Mopidy services, including: - https://dl.mopidy.com/pimusicbox/, which is used to distribute Pi Musicbox images. - - -GlobalSign -========== - -`GlobalSign `_ provides Mopidy with a free SSL -certificate for mopidy.com, which we use to secure access to all our web sites. From 3346778c4118f6e6e3f679d4b1280e4b47b50de3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 7 Aug 2016 21:29:14 +0200 Subject: [PATCH 43/46] models: Fix encoding error in Identifier field Fixes #1508 --- docs/changelog.rst | 4 ++++ mopidy/models/fields.py | 7 +++++-- tests/models/test_fields.py | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d6548865..8a3fd869 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,6 +46,10 @@ Bug fix release. one. Particularly relevant for Mopidy-Scrobbler users, as before it was essentially unusable. (Fixes: :issue:`1456`, PR: :issue:`1534`) +- Models: Fix encoding error if :class:`~mopidy.models.fields.Identifier` + fields, like the ``musicbrainz_id`` model fields, contained non-ASCII Unicode + data. (Fixes: :issue:`1508`, PR: :issue:`1546`) + - File: Ensure path comparision is done between bytestrings only. Fixes crash where a :confval:`file/media_dirs` path contained non-ASCII characters. (Fixes: :issue:`1345`, PR: :issue:`1493`) diff --git a/mopidy/models/fields.py b/mopidy/models/fields.py index c686b447..c5800eba 100644 --- a/mopidy/models/fields.py +++ b/mopidy/models/fields.py @@ -88,14 +88,17 @@ class Date(String): class Identifier(String): """ - :class:`Field` for storing ASCII values such as GUIDs or other identifiers. + :class:`Field` for storing values such as GUIDs or other identifiers. Values will be interned. :param default: default value for field """ def validate(self, value): - return compat.intern(str(super(Identifier, self).validate(value))) + value = super(Identifier, self).validate(value) + if isinstance(value, compat.text_type): + value = value.encode('utf-8') + return compat.intern(value) class URI(Identifier): diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py index 69f4cd91..a4788e4d 100644 --- a/tests/models/test_fields.py +++ b/tests/models/test_fields.py @@ -1,3 +1,5 @@ +# encoding: utf-8 + from __future__ import absolute_import, unicode_literals import unittest @@ -146,6 +148,11 @@ class IdentifierTest(unittest.TestCase): instance.attr = u'abc' self.assertEqual(u'abc', instance.attr) + def test_unicode_with_nonascii_allowed(self): + instance = create_instance(Identifier()) + instance.attr = u'æøå' + self.assertEqual(u'æøå'.encode('utf-8'), instance.attr) + def test_other_disallowed(self): instance = create_instance(Identifier()) with self.assertRaises(TypeError): From 4c3309814f61a67c4baa2a92914e030ab3c9240e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Aug 2016 01:16:53 +0200 Subject: [PATCH 44/46] docs: Fix typos in changelog --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8a3fd869..f3482ea2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -50,7 +50,7 @@ Bug fix release. fields, like the ``musicbrainz_id`` model fields, contained non-ASCII Unicode data. (Fixes: :issue:`1508`, PR: :issue:`1546`) -- File: Ensure path comparision is done between bytestrings only. Fixes crash +- File: Ensure path comparison is done between bytestrings only. Fixes crash where a :confval:`file/media_dirs` path contained non-ASCII characters. (Fixes: :issue:`1345`, PR: :issue:`1493`) @@ -401,7 +401,7 @@ Bug fix release. proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`) - Stream: Fix bug in new playlist parser. A non-ASCII char in an urilist - comment would cause a crash while parsing due to comparision of a non-ASCII + comment would cause a crash while parsing due to comparison of a non-ASCII bytestring with a Unicode string. (Fixes: :issue:`1265`) - File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real From 1ec465bb5dfd75d38588cf3e5960319309ec17c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Aug 2016 20:50:19 +0200 Subject: [PATCH 45/46] docs: Add v2.0.1 release date --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f3482ea2..97ac8676 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.1 (UNRELEASED) +v2.0.1 (2016-08-16) =================== Bug fix release. From 30115192312c9da764c9b22439f59f646f917aef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 12 Aug 2016 20:51:15 +0200 Subject: [PATCH 46/46] Bump version to 2.0.1 --- docs/releasing.rst | 3 +-- mopidy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/releasing.rst b/docs/releasing.rst index 8d489146..5bd4dfeb 100644 --- a/docs/releasing.rst +++ b/docs/releasing.rst @@ -13,8 +13,7 @@ Creating releases #. Update changelog and commit it. -#. Bump the version number in ``mopidy/__init__.py``. Remember to update the - test case in ``tests/test_version.py``. +#. Bump the version number in ``mopidy/__init__.py``. #. Merge the release branch (``develop`` in the example) into master:: diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 4a6370e8..184f5991 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -14,4 +14,4 @@ if not (2, 7) <= sys.version_info < (3,): warnings.filterwarnings('ignore', 'could not open display') -__version__ = '2.0.0' +__version__ = '2.0.1'