From d046974aaf520af38ea8475055f2a239f8fee90c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 1 Sep 2015 23:58:48 +0200 Subject: [PATCH 01/92] gst1: Remove IcySrc It was a workaround for icy:// support on GStreamer 0.10. --- mopidy/audio/actor.py | 4 +-- mopidy/audio/icy.py | 63 ------------------------------------------- 2 files changed, 1 insertion(+), 66 deletions(-) delete mode 100644 mopidy/audio/icy.py diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index b8b3d9a4..9645c4af 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -14,7 +14,7 @@ import gst.pbutils # noqa import pykka from mopidy import exceptions -from mopidy.audio import icy, utils +from mopidy.audio import utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process @@ -27,8 +27,6 @@ logger = logging.getLogger(__name__) # set_state on a pipeline. gst_logger = logging.getLogger('mopidy.audio.gst') -icy.register() - _GST_STATE_MAPPING = { gst.STATE_PLAYING: PlaybackState.PLAYING, gst.STATE_PAUSED: PlaybackState.PAUSED, diff --git a/mopidy/audio/icy.py b/mopidy/audio/icy.py deleted file mode 100644 index dd59baae..00000000 --- a/mopidy/audio/icy.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa - - -class IcySrc(gst.Bin, gst.URIHandler): - __gstdetails__ = ('IcySrc', - 'Src', - 'HTTP src wrapper for icy:// support.', - 'Mopidy') - - srcpad_template = gst.PadTemplate( - 'src', gst.PAD_SRC, gst.PAD_ALWAYS, - gst.caps_new_any()) - - __gsttemplates__ = (srcpad_template,) - - def __init__(self): - super(IcySrc, self).__init__() - self._httpsrc = gst.element_make_from_uri(gst.URI_SRC, 'http://') - try: - self._httpsrc.set_property('iradio-mode', True) - except TypeError: - pass - self.add(self._httpsrc) - - self._srcpad = gst.GhostPad('src', self._httpsrc.get_pad('src')) - self.add_pad(self._srcpad) - - @classmethod - def do_get_type_full(cls): - return gst.URI_SRC - - @classmethod - def do_get_protocols_full(cls): - return [b'icy', b'icyx'] - - def do_set_uri(self, uri): - if uri.startswith('icy://'): - return self._httpsrc.set_uri(b'http://' + uri[len('icy://'):]) - elif uri.startswith('icyx://'): - return self._httpsrc.set_uri(b'https://' + uri[len('icyx://'):]) - else: - return False - - def do_get_uri(self): - uri = self._httpsrc.get_uri() - if uri.startswith('http://'): - return b'icy://' + uri[len('http://'):] - else: - return b'icyx://' + uri[len('https://'):] - - -def register(): - # Only register icy if gst install can't handle it on it's own. - if not gst.element_make_from_uri(gst.URI_SRC, 'icy://'): - gobject.type_register(IcySrc) - gst.element_register( - IcySrc, IcySrc.__name__.lower(), gst.RANK_MARGINAL) From 8c82f4773ffd48eec76a19bd78d76cb0e9f27a2d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:24:02 +0200 Subject: [PATCH 02/92] gst1: Update imports to use PyGI --- mopidy/__main__.py | 8 +- mopidy/audio/actor.py | 161 +++++++++++----------- mopidy/audio/scan.py | 69 +++++----- mopidy/audio/utils.py | 64 ++++----- mopidy/commands.py | 8 +- mopidy/internal/deps.py | 17 ++- mopidy/internal/network.py | 28 ++-- mopidy/internal/playlists.py | 4 - tests/audio/test_actor.py | 42 +++--- tests/audio/test_scan.py | 8 +- tests/internal/network/test_connection.py | 96 ++++++------- tests/internal/network/test_server.py | 12 +- tests/internal/test_deps.py | 17 ++- tests/internal/test_path.py | 4 +- 14 files changed, 263 insertions(+), 275 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index fbc750af..c1cf42f9 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -7,12 +7,12 @@ import sys import textwrap try: - import gobject # noqa + from gi.repository import GObject, Gst except ImportError: print(textwrap.dedent(""" - ERROR: The gobject Python package was not found. + ERROR: The GObject and Gst Python packages were not found. - Mopidy requires GStreamer (and GObject) to work. These are C libraries + Mopidy requires GStreamer and GObject to work. These are C libraries with a number of dependencies themselves, and cannot be installed with the regular Python tools like pip. @@ -21,7 +21,7 @@ except ImportError: """)) raise -gobject.threads_init() +GObject.threads_init() try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 9645c4af..3595092e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -4,12 +4,9 @@ import logging import os import threading -import gobject - -import pygst -pygst.require('0.10') -import gst # noqa -import gst.pbutils # noqa +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst import pykka @@ -28,9 +25,9 @@ logger = logging.getLogger(__name__) gst_logger = logging.getLogger('mopidy.audio.gst') _GST_STATE_MAPPING = { - gst.STATE_PLAYING: PlaybackState.PLAYING, - gst.STATE_PAUSED: PlaybackState.PAUSED, - gst.STATE_NULL: PlaybackState.STOPPED} + Gst.STATE_PLAYING: PlaybackState.PLAYING, + Gst.STATE_PAUSED: PlaybackState.PAUSED, + Gst.STATE_NULL: PlaybackState.STOPPED} class _Signals(object): @@ -118,9 +115,9 @@ class _Appsrc(object): if buffer_ is None: gst_logger.debug('Sending appsrc end-of-stream event.') - return self._source.emit('end-of-stream') == gst.FLOW_OK + return self._source.emit('end-of-stream') == Gst.FLOW_OK else: - return self._source.emit('push-buffer', buffer_) == gst.FLOW_OK + return self._source.emit('push-buffer', buffer_) == Gst.FLOW_OK def _on_signal(self, element, clocktime, func): # This shim is used to ensure we always return true, and also handles @@ -133,29 +130,29 @@ class _Appsrc(object): # TODO: expose this as a property on audio when #790 gets further along. -class _Outputs(gst.Bin): +class _Outputs(Gst.Bin): def __init__(self): - gst.Bin.__init__(self, 'outputs') + Gst.Bin.__init__(self, 'outputs') - self._tee = gst.element_factory_make('tee') + self._tee = Gst.element_factory_make('tee') self.add(self._tee) - ghost_pad = gst.GhostPad('sink', self._tee.get_pad('sink')) + ghost_pad = Gst.GhostPad('sink', self._tee.get_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee # doesn't fail even if we don't have any outputs. - fakesink = gst.element_factory_make('fakesink') + fakesink = Gst.element_factory_make('fakesink') fakesink.set_property('sync', True) self._add(fakesink) def add_output(self, description): # XXX This only works for pipelines not in use until #790 gets done. try: - output = gst.parse_bin_from_description( + output = Gst.parse_bin_from_description( description, ghost_unconnected_pads=True) - except gobject.GError as ex: + except GObject.GError as ex: logger.error( 'Failed to create audio output "%s": %s', description, ex) raise exceptions.AudioException(bytes(ex)) @@ -164,7 +161,7 @@ class _Outputs(gst.Bin): logger.info('Audio output set to "%s"', description) def _add(self, element): - queue = gst.element_factory_make('queue') + queue = Gst.element_factory_make('queue') self.add(element) self.add(queue) queue.link(element) @@ -234,28 +231,28 @@ class _Handler(object): self._event_handler_id = None def on_message(self, bus, msg): - if msg.type == gst.MESSAGE_STATE_CHANGED and msg.src == self._element: + if msg.type == Gst.MESSAGE_STATE_CHANGED and msg.src == self._element: self.on_playbin_state_changed(*msg.parse_state_changed()) - elif msg.type == gst.MESSAGE_BUFFERING: + elif msg.type == Gst.MESSAGE_BUFFERING: self.on_buffering(msg.parse_buffering(), msg.structure) - elif msg.type == gst.MESSAGE_EOS: + elif msg.type == Gst.MESSAGE_EOS: self.on_end_of_stream() - elif msg.type == gst.MESSAGE_ERROR: + elif msg.type == Gst.MESSAGE_ERROR: self.on_error(*msg.parse_error()) - elif msg.type == gst.MESSAGE_WARNING: + elif msg.type == Gst.MESSAGE_WARNING: self.on_warning(*msg.parse_warning()) - elif msg.type == gst.MESSAGE_ASYNC_DONE: + elif msg.type == Gst.MESSAGE_ASYNC_DONE: self.on_async_done() - elif msg.type == gst.MESSAGE_TAG: + elif msg.type == Gst.MESSAGE_TAG: self.on_tag(msg.parse_tag()) - elif msg.type == gst.MESSAGE_ELEMENT: - if gst.pbutils.is_missing_plugin_message(msg): + elif msg.type == Gst.MESSAGE_ELEMENT: + if Gst.pbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) def on_event(self, pad, event): - if event.type == gst.EVENT_NEWSEGMENT: + if event.type == Gst.EVENT_NEWSEGMENT: self.on_new_segment(*event.parse_new_segment()) - elif event.type == gst.EVENT_SINK_MESSAGE: + elif event.type == Gst.EVENT_SINK_MESSAGE: # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() @@ -268,17 +265,17 @@ class _Handler(object): old_state.value_name, new_state.value_name, pending_state.value_name) - if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL: + if new_state == Gst.STATE_READY and pending_state == Gst.STATE_NULL: # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. - new_state = gst.STATE_NULL - pending_state = gst.STATE_VOID_PENDING + new_state = Gst.STATE_NULL + pending_state = Gst.STATE_VOID_PENDING - if pending_state != gst.STATE_VOID_PENDING: + if pending_state != Gst.STATE_VOID_PENDING: return # Ignore intermediate state changes - if new_state == gst.STATE_READY: + if new_state == Gst.STATE_READY: return # Ignore READY state as it's GStreamer specific new_state = _GST_STATE_MAPPING[new_state] @@ -297,23 +294,23 @@ class _Handler(object): AudioListener.send('stream_changed', uri=None) if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ: - gst.DEBUG_BIN_TO_DOT_FILE( - self._audio._playbin, gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') + Gst.DEBUG_BIN_TO_DOT_FILE( + self._audio._playbin, Gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') def on_buffering(self, percent, structure=None): if structure and structure.has_field('buffering-mode'): - if structure['buffering-mode'] == gst.BUFFERING_LIVE: + if structure['buffering-mode'] == Gst.BUFFERING_LIVE: return # Live sources stall in paused. level = logging.getLevelName('TRACE') if percent < 10 and not self._audio._buffering: - self._audio._playbin.set_state(gst.STATE_PAUSED) + self._audio._playbin.set_state(Gst.STATE_PAUSED) self._audio._buffering = True level = logging.DEBUG if percent == 100: self._audio._buffering = False - if self._audio._target_state == gst.STATE_PLAYING: - self._audio._playbin.set_state(gst.STATE_PLAYING) + if self._audio._target_state == Gst.STATE_PLAYING: + self._audio._playbin.set_state(Gst.STATE_PLAYING) level = logging.DEBUG gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) @@ -346,12 +343,12 @@ class _Handler(object): AudioListener.send('tags_changed', tags=tags.keys()) def on_missing_plugin(self, msg): - desc = gst.pbutils.missing_plugin_message_get_description(msg) - debug = gst.pbutils.missing_plugin_message_get_installer_detail(msg) + desc = Gst.pbutils.missing_plugin_message_get_description(msg) + debug = Gst.pbutils.missing_plugin_message_get_installer_detail(msg) gst_logger.debug('Got missing-plugin message: description:%s', desc) logger.warning('Could not find a %s to handle media.', desc) - if gst.pbutils.install_plugins_supported(): + if Gst.pbutils.install_plugins_supported(): logger.info('You might be able to fix this by running: ' 'gst-installer "%s"', debug) # TODO: store the missing plugins installer info in a file so we can @@ -362,7 +359,7 @@ class _Handler(object): gst_logger.debug('Got new-segment event: update=%s rate=%s format=%s ' 'start=%s stop=%s position=%s', update, rate, format_.value_name, start, stop, position) - position_ms = position // gst.MSECOND + position_ms = position // Gst.MSECOND logger.debug('Audio event: position_changed(position=%s)', position_ms) AudioListener.send('position_changed', position=position_ms) @@ -389,7 +386,7 @@ class Audio(pykka.ThreadingActor): super(Audio, self).__init__() self._config = config - self._target_state = gst.STATE_NULL + self._target_state = Gst.STATE_NULL self._buffering = False self._tags = {} @@ -411,7 +408,7 @@ class Audio(pykka.ThreadingActor): self._setup_playbin() self._setup_outputs() self._setup_audio_sink() - except gobject.GError as ex: + except GObject.GError as ex: logger.exception(ex) process.exit_process() @@ -422,19 +419,19 @@ class Audio(pykka.ThreadingActor): def _setup_preferences(self): # TODO: move out of audio actor? # Fix for https://github.com/mopidy/mopidy/issues/604 - registry = gst.registry_get_default() + registry = Gst.registry_get_default() jacksink = registry.find_feature( - 'jackaudiosink', gst.TYPE_ELEMENT_FACTORY) + 'jackaudiosink', Gst.TYPE_ELEMENT_FACTORY) if jacksink: - jacksink.set_rank(gst.RANK_SECONDARY) + jacksink.set_rank(Gst.RANK_SECONDARY) def _setup_playbin(self): - playbin = gst.element_factory_make('playbin2') + playbin = Gst.element_factory_make('playbin2') playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... playbin.set_property('buffer-size', 5 << 20) # 5MB - playbin.set_property('buffer-duration', 5 * gst.SECOND) + playbin.set_property('buffer-duration', 5 * Gst.SECOND) self._signals.connect(playbin, 'source-setup', self._on_source_setup) self._signals.connect(playbin, 'about-to-finish', @@ -448,13 +445,13 @@ class Audio(pykka.ThreadingActor): self._handler.teardown_event_handling() self._signals.disconnect(self._playbin, 'about-to-finish') self._signals.disconnect(self._playbin, 'source-setup') - self._playbin.set_state(gst.STATE_NULL) + self._playbin.set_state(Gst.STATE_NULL) def _setup_outputs(self): # We don't want to use outputs for regular testing, so just install # an unsynced fakesink when someone asks for a 'testoutput'. if self._config['audio']['output'] == 'testoutput': - self._outputs = gst.element_factory_make('fakesink') + self._outputs = Gst.element_factory_make('fakesink') else: self._outputs = _Outputs() try: @@ -465,23 +462,23 @@ class Audio(pykka.ThreadingActor): self._handler.setup_event_handling(self._outputs.get_pad('sink')) def _setup_audio_sink(self): - audio_sink = gst.Bin('audio-sink') + audio_sink = Gst.Bin('audio-sink') # Queue element to buy us time between the about to finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. # TODO: make the min-max values a setting? - queue = gst.element_factory_make('queue') + queue = Gst.element_factory_make('queue') queue.set_property('max-size-buffers', 0) queue.set_property('max-size-bytes', 0) - queue.set_property('max-size-time', 3 * gst.SECOND) - queue.set_property('min-threshold-time', 1 * gst.SECOND) + queue.set_property('max-size-time', 3 * Gst.SECOND) + queue.set_property('min-threshold-time', 1 * Gst.SECOND) audio_sink.add(queue) audio_sink.add(self._outputs) if self.mixer: - volume = gst.element_factory_make('volume') + volume = Gst.element_factory_make('volume') audio_sink.add(volume) queue.link(volume) volume.link(self._outputs) @@ -489,7 +486,7 @@ class Audio(pykka.ThreadingActor): else: queue.link(self._outputs) - ghost_pad = gst.GhostPad('sink', queue.get_pad('sink')) + ghost_pad = Gst.GhostPad('sink', queue.get_pad('sink')) audio_sink.add_pad(ghost_pad) self._playbin.set_property('audio-sink', audio_sink) @@ -561,7 +558,7 @@ class Audio(pykka.ThreadingActor): :type seek_data: callable which takes time position in ms """ self._appsrc.prepare( - gst.Caps(bytes(caps)), need_data, enough_data, seek_data) + Gst.Caps(bytes(caps)), need_data, enough_data, seek_data) self._playbin.set_property('uri', 'appsrc://') def emit_data(self, buffer_): @@ -577,7 +574,7 @@ class Audio(pykka.ThreadingActor): Returns :class:`True` if data was delivered. :param buffer_: buffer to pass to appsrc - :type buffer_: :class:`gst.Buffer` or :class:`None` + :type buffer_: :class:`Gst.Buffer` or :class:`None` :rtype: boolean """ return self._appsrc.push(buffer_) @@ -616,9 +613,9 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0] + gst_position = self._playbin.query_position(Gst.FORMAT_TIME)[0] return utils.clocktime_to_millisecond(gst_position) - except gst.QueryError: + except Gst.QueryError: # TODO: take state into account for this and possibly also return # None as the unknown value instead of zero? logger.debug('Position query failed') @@ -635,7 +632,7 @@ class Audio(pykka.ThreadingActor): # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) result = self._playbin.seek_simple( - gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position) + Gst.Format(Gst.FORMAT_TIME), Gst.SEEK_FLAG_FLUSH, gst_position) gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result @@ -645,7 +642,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(gst.STATE_PLAYING) + return self._set_state(Gst.STATE_PLAYING) def pause_playback(self): """ @@ -653,7 +650,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(gst.STATE_PAUSED) + return self._set_state(Gst.STATE_PAUSED) def prepare_change(self): """ @@ -664,7 +661,7 @@ class Audio(pykka.ThreadingActor): is that GStreamer will reset all its state when it changes to :attr:`gst.STATE_READY`. """ - return self._set_state(gst.STATE_READY) + return self._set_state(Gst.STATE_READY) def stop_playback(self): """ @@ -673,7 +670,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ self._buffering = False - return self._set_state(gst.STATE_NULL) + return self._set_state(Gst.STATE_NULL) def wait_for_state_change(self): """Block until any pending state changes are complete. @@ -689,7 +686,7 @@ class Audio(pykka.ThreadingActor): """ def sync_handler(bus, message): self._handler.on_message(bus, message) - return gst.BUS_DROP + return Gst.BUS_DROP bus = self._playbin.get_bus() bus.set_sync_handler(sync_handler) @@ -710,9 +707,9 @@ class Audio(pykka.ThreadingActor): "READY" -> "NULL" "READY" -> "PAUSED" - :param state: State to set playbin to. One of: `gst.STATE_NULL`, - `gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. - :type state: :class:`gst.State` + :param state: State to set playbin to. One of: `Gst.STATE_NULL`, + `Gst.STATE_READY`, `Gst.STATE_PAUSED` and `Gst.STATE_PLAYING`. + :type state: :class:`Gst.State` :rtype: :class:`True` if successfull, else :class:`False` """ self._target_state = state @@ -720,7 +717,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('State change to %s: result=%s', state.value_name, result.value_name) - if result == gst.STATE_CHANGE_FAILURE: + if result == Gst.STATE_CHANGE_FAILURE: logger.warning( 'Setting GStreamer state to %s failed', state.value_name) return False @@ -740,25 +737,25 @@ class Audio(pykka.ThreadingActor): :param track: the current track :type track: :class:`mopidy.models.Track` """ - taglist = gst.TagList() + taglist = Gst.TagList() artists = [a for a in (track.artists or []) if a.name] # Default to blank data to trick shoutcast into clearing any previous # values it might have. - taglist[gst.TAG_ARTIST] = ' ' - taglist[gst.TAG_TITLE] = ' ' - taglist[gst.TAG_ALBUM] = ' ' + taglist[Gst.TAG_ARTIST] = ' ' + taglist[Gst.TAG_TITLE] = ' ' + taglist[Gst.TAG_ALBUM] = ' ' if artists: - taglist[gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) + taglist[Gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) if track.name: - taglist[gst.TAG_TITLE] = track.name + taglist[Gst.TAG_TITLE] = track.name if track.album and track.album.name: - taglist[gst.TAG_ALBUM] = track.album.name + taglist[Gst.TAG_ALBUM] = track.album.name - event = gst.event_new_tag(taglist) + event = Gst.event_new_tag(taglist) # TODO: check if we get this back on our own bus? self._playbin.send_event(event) gst_logger.debug('Sent tag event: track=%s', track.uri) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fd5d2d49..ba6adaf0 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -3,10 +3,9 @@ from __future__ import ( import collections -import pygst -pygst.require('0.10') -import gst # noqa -import gst.pbutils # noqa +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst, GstPbutils from mopidy import exceptions from mopidy.audio import utils @@ -15,7 +14,7 @@ from mopidy.internal import encoding _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) -_RAW_AUDIO = gst.Caps(b'audio/x-raw-int; audio/x-raw-float') +_RAW_AUDIO = Gst.Caps(b'audio/x-raw-int; audio/x-raw-float') # TODO: replace with a scan(uri, timeout=1000, proxy_config=None)? @@ -59,7 +58,7 @@ class Scanner(object): duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: - pipeline.set_state(gst.STATE_NULL) + pipeline.set_state(Gst.STATE_NULL) del pipeline return _Result(uri, tags, duration, seekable, mime, have_audio) @@ -68,17 +67,17 @@ class Scanner(object): # Turns out it's _much_ faster to just create a new pipeline for every as # decodebins and other elements don't seem to take well to being reused. def _setup_pipeline(uri, proxy_config=None): - src = gst.element_make_from_uri(gst.URI_SRC, uri) + src = Gst.element_make_from_uri(Gst.URI_SRC, uri) if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) - typefind = gst.element_factory_make('typefind') - decodebin = gst.element_factory_make('decodebin2') + typefind = Gst.element_factory_make('typefind') + decodebin = Gst.element_factory_make('decodebin2') - pipeline = gst.element_factory_make('pipeline') + pipeline = Gst.element_factory_make('pipeline') for e in (src, typefind, decodebin): pipeline.add(e) - gst.element_link_many(src, typefind, decodebin) + Gst.element_link_many(src, typefind, decodebin) if proxy_config: utils.setup_proxy(src, proxy_config) @@ -91,13 +90,13 @@ def _setup_pipeline(uri, proxy_config=None): def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) - struct = gst.Structure('have-type') + struct = Gst.Structure('have-type') struct['caps'] = caps.get_structure(0) - element.get_bus().post(gst.message_new_application(element, struct)) + element.get_bus().post(Gst.message_new_application(element, struct)) def _pad_added(element, pad, pipeline): - sink = gst.element_factory_make('fakesink') + sink = Gst.element_factory_make('fakesink') sink.set_property('sync', False) pipeline.add(sink) @@ -105,29 +104,29 @@ def _pad_added(element, pad, pipeline): pad.link(sink.get_pad('sink')) if pad.get_caps().is_subset(_RAW_AUDIO): - struct = gst.Structure('have-audio') - element.get_bus().post(gst.message_new_application(element, struct)) + struct = Gst.Structure('have-audio') + element.get_bus().post(Gst.message_new_application(element, struct)) def _start_pipeline(pipeline): - if pipeline.set_state(gst.STATE_PAUSED) == gst.STATE_CHANGE_NO_PREROLL: - pipeline.set_state(gst.STATE_PLAYING) + if pipeline.set_state(Gst.STATE_PAUSED) == Gst.STATE_CHANGE_NO_PREROLL: + pipeline.set_state(Gst.STATE_PLAYING) def _query_duration(pipeline): try: - duration = pipeline.query_duration(gst.FORMAT_TIME, None)[0] - except gst.QueryError: + duration = pipeline.query_duration(Gst.FORMAT_TIME, None)[0] + except Gst.QueryError: return None if duration < 0: return None else: - return duration // gst.MSECOND + return duration // Gst.MSECOND def _query_seekable(pipeline): - query = gst.query_new_seeking(gst.FORMAT_TIME) + query = Gst.query_new_seeking(Gst.FORMAT_TIME) pipeline.query(query) return query.parse_seeking()[1] @@ -135,15 +134,15 @@ def _query_seekable(pipeline): def _process(pipeline, timeout_ms): clock = pipeline.get_clock() bus = pipeline.get_bus() - timeout = timeout_ms * gst.MSECOND + timeout = timeout_ms * Gst.MSECOND tags = {} mime = None have_audio = False missing_message = None types = ( - gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | - gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) + Gst.MESSAGE_ELEMENT | Gst.MESSAGE_APPLICATION | Gst.MESSAGE_ERROR | + Gst.MESSAGE_EOS | Gst.MESSAGE_ASYNC_DONE | Gst.MESSAGE_TAG) previous = clock.get_time() while timeout > 0: @@ -151,29 +150,29 @@ def _process(pipeline, timeout_ms): if message is None: break - elif message.type == gst.MESSAGE_ELEMENT: - if gst.pbutils.is_missing_plugin_message(message): + elif message.type == Gst.MESSAGE_ELEMENT: + if GstPbutils.is_missing_plugin_message(message): missing_message = message - elif message.type == gst.MESSAGE_APPLICATION: + elif message.type == Gst.MESSAGE_APPLICATION: if message.structure.get_name() == 'have-type': mime = message.structure['caps'].get_name() if mime.startswith('text/') or mime == 'application/xml': return tags, mime, have_audio elif message.structure.get_name() == 'have-audio': have_audio = True - elif message.type == gst.MESSAGE_ERROR: + elif message.type == Gst.MESSAGE_ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_message and not mime: caps = missing_message.structure['detail'] mime = caps.get_structure(0).get_name() return tags, mime, have_audio raise exceptions.ScannerError(error) - elif message.type == gst.MESSAGE_EOS: + elif message.type == Gst.MESSAGE_EOS: return tags, mime, have_audio - elif message.type == gst.MESSAGE_ASYNC_DONE: + elif message.type == Gst.MESSAGE_ASYNC_DONE: if message.src == pipeline: return tags, mime, have_audio - elif message.type == gst.MESSAGE_TAG: + elif message.type == Gst.MESSAGE_TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) @@ -189,15 +188,13 @@ if __name__ == '__main__': import os import sys - import gobject - from mopidy.internal import path - gobject.threads_init() + GObject.threads_init() scanner = Scanner(5000) for uri in sys.argv[1:]: - if not gst.uri_is_valid(uri): + if not Gst.uri_is_valid(uri): uri = path.path_to_uri(os.path.abspath(uri)) try: result = scanner.scan(uri) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index bc527df7..aa0b1d63 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -4,9 +4,9 @@ import datetime import logging import numbers -import pygst -pygst.require('0.10') -import gst # noqa +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst from mopidy import compat, httpclient from mopidy.models import Album, Artist, Track @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) def calculate_duration(num_samples, sample_rate): """Determine duration of samples using GStreamer helper for precise math.""" - return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate) + return Gst.util_uint64_scale(num_samples, Gst.SECOND, sample_rate) def create_buffer(data, capabilites=None, timestamp=None, duration=None): @@ -25,10 +25,10 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): Mainly intended to keep gst imports out of non-audio modules. """ - buffer_ = gst.Buffer(data) + buffer_ = Gst.Buffer(data) if capabilites: if isinstance(capabilites, compat.string_types): - capabilites = gst.caps_from_string(capabilites) + capabilites = Gst.caps_from_string(capabilites) buffer_.set_caps(capabilites) if timestamp: buffer_.timestamp = timestamp @@ -39,12 +39,12 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): def millisecond_to_clocktime(value): """Convert a millisecond time to internal GStreamer time.""" - return value * gst.MSECOND + return value * Gst.MSECOND def clocktime_to_millisecond(value): """Convert an internal GStreamer time to millisecond time.""" - return value // gst.MSECOND + return value // Gst.MSECOND def supported_uri_schemes(uri_schemes): @@ -55,9 +55,9 @@ def supported_uri_schemes(uri_schemes): :rtype: set of URI schemes we can support via this GStreamer install. """ supported_schemes = set() - registry = gst.registry_get_default() + registry = Gst.registry_get_default() - for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): + for factory in registry.get_feature_list(Gst.TYPE_ELEMENT_FACTORY): for uri in factory.get_uri_protocols(): if uri in uri_schemes: supported_schemes.add(uri) @@ -95,37 +95,37 @@ def convert_tags_to_track(tags): album_kwargs = {} track_kwargs = {} - track_kwargs['composers'] = _artists(tags, gst.TAG_COMPOSER) - track_kwargs['performers'] = _artists(tags, gst.TAG_PERFORMER) - track_kwargs['artists'] = _artists(tags, gst.TAG_ARTIST, + track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER) + track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER) + track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST, 'musicbrainz-artistid', 'musicbrainz-sortname') album_kwargs['artists'] = _artists( - tags, gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') + tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') - track_kwargs['genre'] = '; '.join(tags.get(gst.TAG_GENRE, [])) - track_kwargs['name'] = '; '.join(tags.get(gst.TAG_TITLE, [])) + track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, [])) + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, [])) if not track_kwargs['name']: - track_kwargs['name'] = '; '.join(tags.get(gst.TAG_ORGANIZATION, [])) + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, [])) track_kwargs['comment'] = '; '.join(tags.get('comment', [])) if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_LOCATION, [])) + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, [])) if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_COPYRIGHT, [])) + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, [])) - track_kwargs['track_no'] = tags.get(gst.TAG_TRACK_NUMBER, [None])[0] - track_kwargs['disc_no'] = tags.get(gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] - track_kwargs['bitrate'] = tags.get(gst.TAG_BITRATE, [None])[0] + track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0] + track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] + track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0] track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0] - album_kwargs['name'] = tags.get(gst.TAG_ALBUM, [None])[0] - album_kwargs['num_tracks'] = tags.get(gst.TAG_TRACK_COUNT, [None])[0] - album_kwargs['num_discs'] = tags.get(gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] + album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0] + album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0] + album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - if tags.get(gst.TAG_DATE) and tags.get(gst.TAG_DATE)[0]: - track_kwargs['date'] = tags[gst.TAG_DATE][0].isoformat() + if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: + track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} @@ -142,7 +142,7 @@ def setup_proxy(element, config): """Configure a GStreamer element with proxy settings. :param element: element to setup proxy in. - :type element: :class:`gst.GstElement` + :type element: :class:`Gst.GstElement` :param config: proxy settings to use. :type config: :class:`dict` """ @@ -155,7 +155,7 @@ def setup_proxy(element, config): def convert_taglist(taglist): - """Convert a :class:`gst.Taglist` to plain Python types. + """Convert a :class:`Gst.Taglist` to plain Python types. Knows how to convert: @@ -172,7 +172,7 @@ def convert_taglist(taglist): 0.10.36/gstreamer/html/gstreamer-GstTagList.html :param taglist: A GStreamer taglist to be converted. - :type taglist: :class:`gst.Taglist` + :type taglist: :class:`Gst.Taglist` :rtype: dictionary of tag keys with a list of values. """ result = {} @@ -187,13 +187,13 @@ def convert_taglist(taglist): values = [values] for value in values: - if isinstance(value, gst.Date): + if isinstance(value, Gst.Date): try: date = datetime.date(value.year, value.month, value.day) result[key].append(date) except ValueError: logger.debug('Ignoring invalid date: %r = %r', key, value) - elif isinstance(value, gst.Buffer): + elif isinstance(value, Gst.Buffer): result[key].append(bytes(value)) elif isinstance( value, (compat.string_types, bool, numbers.Number)): diff --git a/mopidy/commands.py b/mopidy/commands.py index 4890c722..872d5773 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -7,9 +7,7 @@ import logging import os import sys -import glib - -import gobject +from gi.repository import GLib, GObject import pykka @@ -21,7 +19,7 @@ from mopidy.internal import deps, process, timer, versioning logger = logging.getLogger(__name__) _default_config = [] -for base in glib.get_system_config_dirs() + (glib.get_user_config_dir(),): +for base in GLib.get_system_config_dirs() + (GLib.get_user_config_dir(),): _default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf')) DEFAULT_CONFIG = b':'.join(_default_config) @@ -286,7 +284,7 @@ class RootCommand(Command): help='`section/key=value` values to override config options') def run(self, args, config): - loop = gobject.MainLoop() + loop = GObject.MainLoop() mixer_class = self.get_mixer_class(config, args.registry['mixer']) backend_classes = args.registry['backend'] diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 1f363657..3744db87 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -5,11 +5,11 @@ import os import platform import sys -import pkg_resources +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst -import pygst -pygst.require('0.10') -import gst # noqa +import pkg_resources from mopidy.internal import formatting @@ -110,8 +110,7 @@ def pkg_info(project_name=None, include_extras=False): def gstreamer_info(): other = [] - other.append('Python wrapper: gst-python %s' % ( - '.'.join(map(str, gst.get_pygst_version())))) + other.append('Python wrapper: python-gi %s' % gi.__version__) found_elements = [] missing_elements = [] @@ -135,8 +134,8 @@ def gstreamer_info(): return { 'name': 'GStreamer', - 'version': '.'.join(map(str, gst.get_gst_version())), - 'path': os.path.dirname(gst.__file__), + 'version': '.'.join(map(str, Gst.version())), + 'path': os.path.dirname(gi.__file__), 'other': '\n'.join(other), } @@ -187,6 +186,6 @@ def _gstreamer_check_elements(): ] known_elements = [ factory.get_name() for factory in - gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)] + Gst.registry_get_default().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] return [ (element, element in known_elements) for element in elements_to_check] diff --git a/mopidy/internal/network.py b/mopidy/internal/network.py index 4b8b35fe..c956d795 100644 --- a/mopidy/internal/network.py +++ b/mopidy/internal/network.py @@ -7,7 +7,7 @@ import socket import sys import threading -import gobject +from gi.repository import GObject import pykka @@ -67,7 +67,7 @@ def format_hostname(hostname): class Server(object): - """Setup listener and register it with gobject's event loop.""" + """Setup listener and register it with GObject's event loop.""" def __init__(self, host, port, protocol, protocol_kwargs=None, max_connections=5, timeout=30): @@ -87,7 +87,7 @@ class Server(object): return sock def register_server_socket(self, fileno): - gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection) + GObject.io_add_watch(fileno, GObject.IO_IN, self.handle_connection) def handle_connection(self, fd, flags): try: @@ -132,7 +132,7 @@ class Server(object): class Connection(object): # NOTE: the callback code is _not_ run in the actor's thread, but in the # same one as the event loop. If code in the callbacks blocks, the rest of - # gobject code will likely be blocked as well... + # GObject code will likely be blocked as well... # # Also note that source_remove() return values are ignored on purpose, a # false return value would only tell us that what we thought was registered @@ -211,14 +211,14 @@ class Connection(object): return self.disable_timeout() - self.timeout_id = gobject.timeout_add_seconds( + self.timeout_id = GObject.timeout_add_seconds( self.timeout, self.timeout_callback) def disable_timeout(self): """Deactivate timeout mechanism.""" if self.timeout_id is None: return - gobject.source_remove(self.timeout_id) + GObject.source_remove(self.timeout_id) self.timeout_id = None def enable_recv(self): @@ -226,9 +226,9 @@ class Connection(object): return try: - self.recv_id = gobject.io_add_watch( + self.recv_id = GObject.io_add_watch( self.sock.fileno(), - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP, self.recv_callback) except socket.error as e: self.stop('Problem with connection: %s' % e) @@ -236,7 +236,7 @@ class Connection(object): def disable_recv(self): if self.recv_id is None: return - gobject.source_remove(self.recv_id) + GObject.source_remove(self.recv_id) self.recv_id = None def enable_send(self): @@ -244,9 +244,9 @@ class Connection(object): return try: - self.send_id = gobject.io_add_watch( + self.send_id = GObject.io_add_watch( self.sock.fileno(), - gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP, self.send_callback) except socket.error as e: self.stop('Problem with connection: %s' % e) @@ -255,11 +255,11 @@ class Connection(object): if self.send_id is None: return - gobject.source_remove(self.send_id) + GObject.source_remove(self.send_id) self.send_id = None def recv_callback(self, fd, flags): - if flags & (gobject.IO_ERR | gobject.IO_HUP): + if flags & (GObject.IO_ERR | GObject.IO_HUP): self.stop('Bad client flags: %s' % flags) return True @@ -283,7 +283,7 @@ class Connection(object): return True def send_callback(self, fd, flags): - if flags & (gobject.IO_ERR | gobject.IO_HUP): + if flags & (GObject.IO_ERR | GObject.IO_HUP): self.stop('Bad client flags: %s' % flags) return True diff --git a/mopidy/internal/playlists.py b/mopidy/internal/playlists.py index f8e654af..e80588c9 100644 --- a/mopidy/internal/playlists.py +++ b/mopidy/internal/playlists.py @@ -2,10 +2,6 @@ from __future__ import absolute_import, unicode_literals import io -import pygst -pygst.require('0.10') -import gst # noqa - from mopidy.compat import configparser from mopidy.internal import validation diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 0cfbdaf3..e1841561 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -3,15 +3,13 @@ from __future__ import absolute_import, unicode_literals import threading import unittest -import gobject -gobject.threads_init() +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() import mock -import pygst -pygst.require('0.10') -import gst # noqa - import pykka from mopidy import audio @@ -520,17 +518,17 @@ class AudioStateTest(unittest.TestCase): def test_state_does_not_change_when_in_gst_ready_state(self): self.audio._handler.on_playbin_state_changed( - gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING) + Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) def test_state_changes_from_stopped_to_playing_on_play(self): self.audio._handler.on_playbin_state_changed( - gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING) + Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_PLAYING) self.audio._handler.on_playbin_state_changed( - gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING) + Gst.STATE_READY, Gst.STATE_PAUSED, Gst.STATE_PLAYING) self.audio._handler.on_playbin_state_changed( - gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING) + Gst.STATE_PAUSED, Gst.STATE_PLAYING, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state) @@ -538,7 +536,7 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING) + Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state) @@ -546,12 +544,12 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL) + Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_NULL) self.audio._handler.on_playbin_state_changed( - gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL) + Gst.STATE_PAUSED, Gst.STATE_READY, Gst.STATE_NULL) # We never get the following call, so the logic must work without it # self.audio._handler.on_playbin_state_changed( - # gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING) + # Gst.STATE_READY, Gst.STATE_NULL, Gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) @@ -565,17 +563,17 @@ class AudioBufferingTest(unittest.TestCase): def test_pause_when_buffer_empty(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.STATE_PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) self.assertTrue(self.audio._buffering) def test_stay_paused_when_buffering_finished(self): playbin = self.audio._playbin self.audio.pause_playback() - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) playbin.set_state.reset_mock() self.audio._handler.on_buffering(100) @@ -585,11 +583,11 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_paused_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.STATE_PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) self.audio.pause_playback() playbin.set_state.reset_mock() @@ -600,13 +598,13 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_stopped_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.STATE_PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.STATE_PAUSED) playbin.set_state.reset_mock() self.audio.stop_playback() - playbin.set_state.assert_called_with(gst.STATE_NULL) + playbin.set_state.assert_called_with(Gst.STATE_NULL) self.assertFalse(self.audio._buffering) diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 8c2b9af3..08def2af 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -3,8 +3,12 @@ from __future__ import absolute_import, unicode_literals import os import unittest -import gobject -gobject.threads_init() +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +GObject.threads_init() +Gst.init(None) from mopidy import exceptions from mopidy.audio import scan diff --git a/tests/internal/network/test_connection.py b/tests/internal/network/test_connection.py index 8ae7d15c..291bbc46 100644 --- a/tests/internal/network/test_connection.py +++ b/tests/internal/network/test_connection.py @@ -5,7 +5,7 @@ import logging import socket import unittest -import gobject +from gi.repository import GObject from mock import Mock, call, patch, sentinel @@ -162,27 +162,27 @@ class ConnectionTest(unittest.TestCase): network.Connection.stop(self.mock, sentinel.reason) network.logger.log(any_int, any_unicode) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_recv_registers_with_gobject(self): self.mock.recv_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno - gobject.io_add_watch.return_value = sentinel.tag + GObject.io_add_watch.return_value = sentinel.tag network.Connection.enable_recv(self.mock) - gobject.io_add_watch.assert_called_once_with( + GObject.io_add_watch.assert_called_once_with( sentinel.fileno, - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP, self.mock.recv_callback) self.assertEqual(sentinel.tag, self.mock.recv_id) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_recv_already_registered(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.recv_id = sentinel.tag network.Connection.enable_recv(self.mock) - self.assertEqual(0, gobject.io_add_watch.call_count) + self.assertEqual(0, GObject.io_add_watch.call_count) def test_enable_recv_does_not_change_tag(self): self.mock.recv_id = sentinel.tag @@ -191,20 +191,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_recv(self.mock) self.assertEqual(sentinel.tag, self.mock.recv_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_recv_deregisters(self): self.mock.recv_id = sentinel.tag network.Connection.disable_recv(self.mock) - gobject.source_remove.assert_called_once_with(sentinel.tag) + GObject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.recv_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_recv_already_deregistered(self): self.mock.recv_id = None network.Connection.disable_recv(self.mock) - self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(0, GObject.source_remove.call_count) self.assertEqual(None, self.mock.recv_id) def test_enable_recv_on_closed_socket(self): @@ -216,27 +216,27 @@ class ConnectionTest(unittest.TestCase): self.mock.stop.assert_called_once_with(any_unicode) self.assertEqual(None, self.mock.recv_id) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_send_registers_with_gobject(self): self.mock.send_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno - gobject.io_add_watch.return_value = sentinel.tag + GObject.io_add_watch.return_value = sentinel.tag network.Connection.enable_send(self.mock) - gobject.io_add_watch.assert_called_once_with( + GObject.io_add_watch.assert_called_once_with( sentinel.fileno, - gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, + GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP, self.mock.send_callback) self.assertEqual(sentinel.tag, self.mock.send_id) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_enable_send_already_registered(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.send_id = sentinel.tag network.Connection.enable_send(self.mock) - self.assertEqual(0, gobject.io_add_watch.call_count) + self.assertEqual(0, GObject.io_add_watch.call_count) def test_enable_send_does_not_change_tag(self): self.mock.send_id = sentinel.tag @@ -245,20 +245,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_send(self.mock) self.assertEqual(sentinel.tag, self.mock.send_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_send_deregisters(self): self.mock.send_id = sentinel.tag network.Connection.disable_send(self.mock) - gobject.source_remove.assert_called_once_with(sentinel.tag) + GObject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.send_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_send_already_deregistered(self): self.mock.send_id = None network.Connection.disable_send(self.mock) - self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(0, GObject.source_remove.call_count) self.assertEqual(None, self.mock.send_id) def test_enable_send_on_closed_socket(self): @@ -269,36 +269,36 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_send(self.mock) self.assertEqual(None, self.mock.send_id) - @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + @patch.object(GObject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_clears_existing_timeouts(self): self.mock.timeout = 10 network.Connection.enable_timeout(self.mock) self.mock.disable_timeout.assert_called_once_with() - @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + @patch.object(GObject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_add_gobject_timeout(self): self.mock.timeout = 10 - gobject.timeout_add_seconds.return_value = sentinel.tag + GObject.timeout_add_seconds.return_value = sentinel.tag network.Connection.enable_timeout(self.mock) - gobject.timeout_add_seconds.assert_called_once_with( + GObject.timeout_add_seconds.assert_called_once_with( 10, self.mock.timeout_callback) self.assertEqual(sentinel.tag, self.mock.timeout_id) - @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + @patch.object(GObject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_does_not_add_timeout(self): self.mock.timeout = 0 network.Connection.enable_timeout(self.mock) - self.assertEqual(0, gobject.timeout_add_seconds.call_count) + self.assertEqual(0, GObject.timeout_add_seconds.call_count) self.mock.timeout = -1 network.Connection.enable_timeout(self.mock) - self.assertEqual(0, gobject.timeout_add_seconds.call_count) + self.assertEqual(0, GObject.timeout_add_seconds.call_count) self.mock.timeout = None network.Connection.enable_timeout(self.mock) - self.assertEqual(0, gobject.timeout_add_seconds.call_count) + self.assertEqual(0, GObject.timeout_add_seconds.call_count) def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self): self.mock.timeout = 0 @@ -313,20 +313,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_timeout(self.mock) self.assertEqual(0, self.mock.disable_timeout.call_count) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_timeout_deregisters(self): self.mock.timeout_id = sentinel.tag network.Connection.disable_timeout(self.mock) - gobject.source_remove.assert_called_once_with(sentinel.tag) + GObject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.timeout_id) - @patch.object(gobject, 'source_remove', new=Mock()) + @patch.object(GObject, 'source_remove', new=Mock()) def test_disable_timeout_already_deregistered(self): self.mock.timeout_id = None network.Connection.disable_timeout(self.mock) - self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(0, GObject.source_remove.call_count) self.assertEqual(None, self.mock.timeout_id) def test_queue_send_acquires_and_releases_lock(self): @@ -372,7 +372,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup(self): @@ -380,7 +380,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup_and_io_err(self): @@ -389,7 +389,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, - gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) + GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_sends_data_to_actor(self): @@ -398,7 +398,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.actor_ref.tell.assert_called_once_with( {'received': 'data'}) @@ -409,7 +409,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_gets_no_data(self): @@ -418,7 +418,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.assertEqual(self.mock.mock_calls, [ call.sock.recv(any_int), call.disable_recv(), @@ -431,7 +431,7 @@ class ConnectionTest(unittest.TestCase): for error in (errno.EWOULDBLOCK, errno.EINTR): self.mock.sock.recv.side_effect = socket.error(error, '') self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.assertEqual(0, self.mock.stop.call_count) def test_recv_callback_unrecoverable_error(self): @@ -439,7 +439,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.recv.side_effect = socket.error self.assertTrue(network.Connection.recv_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_err(self): @@ -450,7 +450,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_hup(self): @@ -461,7 +461,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) + self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_hup_and_io_err(self): @@ -473,7 +473,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, - gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) + GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_acquires_and_releases_lock(self): @@ -484,7 +484,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) self.mock.send_lock.release.assert_called_once_with() @@ -496,7 +496,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) self.assertEqual(0, self.mock.sock.send.call_count) @@ -507,7 +507,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send.return_value = '' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.disable_send.assert_called_once_with() self.mock.send.assert_called_once_with('data') self.assertEqual('', self.mock.send_buffer) @@ -519,7 +519,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send.return_value = 'ta' self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + self.mock, sentinel.fd, GObject.IO_IN)) self.mock.send.assert_called_once_with('data') self.assertEqual('ta', self.mock.send_buffer) diff --git a/tests/internal/network/test_server.py b/tests/internal/network/test_server.py index af8effd2..1df25dbc 100644 --- a/tests/internal/network/test_server.py +++ b/tests/internal/network/test_server.py @@ -4,7 +4,7 @@ import errno import socket import unittest -import gobject +from gi.repository import GObject from mock import Mock, patch, sentinel @@ -91,11 +91,11 @@ class ServerTest(unittest.TestCase): network.Server.create_server_socket( self.mock, sentinel.host, sentinel.port) - @patch.object(gobject, 'io_add_watch', new=Mock()) + @patch.object(GObject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): network.Server.register_server_socket(self.mock, sentinel.fileno) - gobject.io_add_watch.assert_called_once_with( - sentinel.fileno, gobject.IO_IN, self.mock.handle_connection) + GObject.io_add_watch.assert_called_once_with( + sentinel.fileno, GObject.IO_IN, self.mock.handle_connection) def test_handle_connection(self): self.mock.accept_connection.return_value = ( @@ -103,7 +103,7 @@ class ServerTest(unittest.TestCase): self.mock.maximum_connections_exceeded.return_value = False self.assertTrue(network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN)) + self.mock, sentinel.fileno, GObject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.init_connection.assert_called_once_with( @@ -116,7 +116,7 @@ class ServerTest(unittest.TestCase): self.mock.maximum_connections_exceeded.return_value = True self.assertTrue(network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN)) + self.mock, sentinel.fileno, GObject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.reject_connection.assert_called_once_with( diff --git a/tests/internal/test_deps.py b/tests/internal/test_deps.py index 27e6f629..ea102b47 100644 --- a/tests/internal/test_deps.py +++ b/tests/internal/test_deps.py @@ -4,14 +4,14 @@ import platform import sys import unittest +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst + import mock import pkg_resources -import pygst -pygst.require('0.10') -import gst # noqa - from mopidy.internal import deps @@ -74,12 +74,11 @@ class DepsTest(unittest.TestCase): self.assertEqual('GStreamer', result['name']) self.assertEqual( - '.'.join(map(str, gst.get_gst_version())), result['version']) - self.assertIn('gst', result['path']) + '.'.join(map(str, Gst.version())), result['version']) + self.assertIn('gi', result['path']) self.assertNotIn('__init__.py', result['path']) - self.assertIn('Python wrapper: gst-python', result['other']) - self.assertIn( - '.'.join(map(str, gst.get_pygst_version())), result['other']) + self.assertIn('Python wrapper: python-gi', result['other']) + self.assertIn(gi.__version__, result['other']) self.assertIn('Relevant elements:', result['other']) @mock.patch('pkg_resources.get_distribution') diff --git a/tests/internal/test_path.py b/tests/internal/test_path.py index 8aa8f7c1..751e7c6e 100644 --- a/tests/internal/test_path.py +++ b/tests/internal/test_path.py @@ -7,7 +7,7 @@ import shutil import tempfile import unittest -import glib +from gi.repository import GLib from mopidy import compat, exceptions from mopidy.internal import path @@ -215,7 +215,7 @@ class ExpandPathTest(unittest.TestCase): def test_xdg_subsititution(self): self.assertEqual( - glib.get_user_data_dir() + b'/foo', + GLib.get_user_data_dir() + b'/foo', path.expand_path(b'$XDG_DATA_DIR/foo')) def test_xdg_subsititution_unknown(self): From 1d269af210858859a346fe20c47ba5fa2e07ca3d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:27:59 +0200 Subject: [PATCH 03/92] gst1: Call Gst.init() and remove sys.argv hack GStreamer no longer use sys.argv directly. If you want GStreamer to handle command line arguments, you must pass them explicitly to Gst.init(). --- mopidy/__main__.py | 10 ++-------- mopidy/audio/scan.py | 1 + tests/audio/test_actor.py | 2 ++ tests/audio/test_scan.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c1cf42f9..c91740a3 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -22,6 +22,7 @@ except ImportError: raise GObject.threads_init() +Gst.init() try: # Make GObject's mainloop the event loop for python-dbus @@ -33,13 +34,6 @@ except ImportError: import pykka.debug - -# Extract any command line arguments. This needs to be done before GStreamer is -# imported, so that GStreamer doesn't hijack e.g. ``--help``. -mopidy_args = sys.argv[1:] -sys.argv[1:] = [] - - from mopidy import commands, config as config_lib, ext from mopidy.internal import encoding, log, path, process, versioning @@ -73,7 +67,7 @@ def main(): data.command.set(extension=data.extension) root_cmd.add_child(data.extension.ext_name, data.command) - args = root_cmd.parse(mopidy_args) + args = root_cmd.parse(sys.argv[1:]) config, config_errors = config_lib.load( args.config_files, diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ba6adaf0..573d2fab 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -191,6 +191,7 @@ if __name__ == '__main__': from mopidy.internal import path GObject.threads_init() + Gst.init() scanner = Scanner(5000) for uri in sys.argv[1:]: diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index e1841561..48d3704b 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -6,7 +6,9 @@ import unittest import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst + GObject.threads_init() +Gst.init() import mock diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 08def2af..ab995285 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -8,7 +8,7 @@ gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst GObject.threads_init() -Gst.init(None) +Gst.init() from mopidy import exceptions from mopidy.audio import scan From f00f24ffded4408c577d5fe1a20fd88a959e816d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:32:11 +0200 Subject: [PATCH 04/92] gst1: Replace element_factory_make() with ElementFactory.make() --- mopidy/audio/actor.py | 14 +++++++------- mopidy/audio/scan.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 3595092e..7dd5971e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -135,7 +135,7 @@ class _Outputs(Gst.Bin): def __init__(self): Gst.Bin.__init__(self, 'outputs') - self._tee = Gst.element_factory_make('tee') + self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) ghost_pad = Gst.GhostPad('sink', self._tee.get_pad('sink')) @@ -143,7 +143,7 @@ class _Outputs(Gst.Bin): # Add an always connected fakesink which respects the clock so the tee # doesn't fail even if we don't have any outputs. - fakesink = Gst.element_factory_make('fakesink') + fakesink = Gst.ElementFactory.make('fakesink') fakesink.set_property('sync', True) self._add(fakesink) @@ -161,7 +161,7 @@ class _Outputs(Gst.Bin): logger.info('Audio output set to "%s"', description) def _add(self, element): - queue = Gst.element_factory_make('queue') + queue = Gst.ElementFactory.make('queue') self.add(element) self.add(queue) queue.link(element) @@ -426,7 +426,7 @@ class Audio(pykka.ThreadingActor): jacksink.set_rank(Gst.RANK_SECONDARY) def _setup_playbin(self): - playbin = Gst.element_factory_make('playbin2') + playbin = Gst.ElementFactory.make('playbin2') playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... @@ -451,7 +451,7 @@ class Audio(pykka.ThreadingActor): # We don't want to use outputs for regular testing, so just install # an unsynced fakesink when someone asks for a 'testoutput'. if self._config['audio']['output'] == 'testoutput': - self._outputs = Gst.element_factory_make('fakesink') + self._outputs = Gst.ElementFactory.make('fakesink') else: self._outputs = _Outputs() try: @@ -468,7 +468,7 @@ class Audio(pykka.ThreadingActor): # the actual switch, i.e. about to switch can block for longer thanks # to this queue. # TODO: make the min-max values a setting? - queue = Gst.element_factory_make('queue') + queue = Gst.ElementFactory.make('queue') queue.set_property('max-size-buffers', 0) queue.set_property('max-size-bytes', 0) queue.set_property('max-size-time', 3 * Gst.SECOND) @@ -478,7 +478,7 @@ class Audio(pykka.ThreadingActor): audio_sink.add(self._outputs) if self.mixer: - volume = Gst.element_factory_make('volume') + volume = Gst.ElementFactory.make('volume') audio_sink.add(volume) queue.link(volume) volume.link(self._outputs) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 573d2fab..3263f035 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -71,10 +71,10 @@ def _setup_pipeline(uri, proxy_config=None): if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) - typefind = Gst.element_factory_make('typefind') - decodebin = Gst.element_factory_make('decodebin2') + typefind = Gst.ElementFactory.make('typefind') + decodebin = Gst.ElementFactory.make('decodebin2') - pipeline = Gst.element_factory_make('pipeline') + pipeline = Gst.ElementFactory.make('pipeline') for e in (src, typefind, decodebin): pipeline.add(e) Gst.element_link_many(src, typefind, decodebin) @@ -96,7 +96,7 @@ def _have_type(element, probability, caps, decodebin): def _pad_added(element, pad, pipeline): - sink = Gst.element_factory_make('fakesink') + sink = Gst.ElementFactory.make('fakesink') sink.set_property('sync', False) pipeline.add(sink) From ab24222eb6e725ff32c169fe641659f74c41606e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:33:02 +0200 Subject: [PATCH 05/92] gst1: Replace gst.element_link_many() --- mopidy/audio/scan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 3263f035..8967a180 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -77,7 +77,8 @@ def _setup_pipeline(uri, proxy_config=None): pipeline = Gst.ElementFactory.make('pipeline') for e in (src, typefind, decodebin): pipeline.add(e) - Gst.element_link_many(src, typefind, decodebin) + src.link(typefind) + typefind.link(decodebin) if proxy_config: utils.setup_proxy(src, proxy_config) From dfaed1e4c23cb397b7b21564b55de14ce44c8dca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:36:59 +0200 Subject: [PATCH 06/92] gst1: Replace STATE_* with State.* --- mopidy/audio/actor.py | 45 ++++++++++++++++++++------------------- mopidy/audio/scan.py | 6 +++--- tests/audio/test_actor.py | 32 ++++++++++++++-------------- 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 7dd5971e..bcd424bf 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -25,9 +25,10 @@ logger = logging.getLogger(__name__) gst_logger = logging.getLogger('mopidy.audio.gst') _GST_STATE_MAPPING = { - Gst.STATE_PLAYING: PlaybackState.PLAYING, - Gst.STATE_PAUSED: PlaybackState.PAUSED, - Gst.STATE_NULL: PlaybackState.STOPPED} + Gst.State.PLAYING: PlaybackState.PLAYING, + Gst.State.PAUSED: PlaybackState.PAUSED, + Gst.State.NULL: PlaybackState.STOPPED, +} class _Signals(object): @@ -265,17 +266,17 @@ class _Handler(object): old_state.value_name, new_state.value_name, pending_state.value_name) - if new_state == Gst.STATE_READY and pending_state == Gst.STATE_NULL: + if new_state == Gst.State.READY and pending_state == Gst.State.NULL: # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. - new_state = Gst.STATE_NULL - pending_state = Gst.STATE_VOID_PENDING + new_state = Gst.State.NULL + pending_state = Gst.State.VOID_PENDING - if pending_state != Gst.STATE_VOID_PENDING: + if pending_state != Gst.State.VOID_PENDING: return # Ignore intermediate state changes - if new_state == Gst.STATE_READY: + if new_state == Gst.State.READY: return # Ignore READY state as it's GStreamer specific new_state = _GST_STATE_MAPPING[new_state] @@ -304,13 +305,13 @@ class _Handler(object): level = logging.getLevelName('TRACE') if percent < 10 and not self._audio._buffering: - self._audio._playbin.set_state(Gst.STATE_PAUSED) + self._audio._playbin.set_state(Gst.State.PAUSED) self._audio._buffering = True level = logging.DEBUG if percent == 100: self._audio._buffering = False - if self._audio._target_state == Gst.STATE_PLAYING: - self._audio._playbin.set_state(Gst.STATE_PLAYING) + if self._audio._target_state == Gst.State.PLAYING: + self._audio._playbin.set_state(Gst.State.PLAYING) level = logging.DEBUG gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) @@ -386,7 +387,7 @@ class Audio(pykka.ThreadingActor): super(Audio, self).__init__() self._config = config - self._target_state = Gst.STATE_NULL + self._target_state = Gst.State.NULL self._buffering = False self._tags = {} @@ -445,7 +446,7 @@ class Audio(pykka.ThreadingActor): self._handler.teardown_event_handling() self._signals.disconnect(self._playbin, 'about-to-finish') self._signals.disconnect(self._playbin, 'source-setup') - self._playbin.set_state(Gst.STATE_NULL) + self._playbin.set_state(Gst.State.NULL) def _setup_outputs(self): # We don't want to use outputs for regular testing, so just install @@ -642,7 +643,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(Gst.STATE_PLAYING) + return self._set_state(Gst.State.PLAYING) def pause_playback(self): """ @@ -650,7 +651,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ - return self._set_state(Gst.STATE_PAUSED) + return self._set_state(Gst.State.PAUSED) def prepare_change(self): """ @@ -659,9 +660,9 @@ class Audio(pykka.ThreadingActor): This function *MUST* be called before changing URIs or doing changes like updating data that is being pushed. The reason for this is that GStreamer will reset all its state when it changes to - :attr:`gst.STATE_READY`. + :attr:`Gst.State.READY`. """ - return self._set_state(Gst.STATE_READY) + return self._set_state(Gst.State.READY) def stop_playback(self): """ @@ -670,7 +671,7 @@ class Audio(pykka.ThreadingActor): :rtype: :class:`True` if successfull, else :class:`False` """ self._buffering = False - return self._set_state(Gst.STATE_NULL) + return self._set_state(Gst.State.NULL) def wait_for_state_change(self): """Block until any pending state changes are complete. @@ -695,7 +696,7 @@ class Audio(pykka.ThreadingActor): """ Internal method for setting the raw GStreamer state. - .. digraph:: gst_state_transitions + .. digraph:: Gst.State.transitions graph [rankdir="LR"]; node [fontsize=10]; @@ -707,8 +708,8 @@ class Audio(pykka.ThreadingActor): "READY" -> "NULL" "READY" -> "PAUSED" - :param state: State to set playbin to. One of: `Gst.STATE_NULL`, - `Gst.STATE_READY`, `Gst.STATE_PAUSED` and `Gst.STATE_PLAYING`. + :param state: State to set playbin to. One of: `Gst.State.NULL`, + `Gst.State.READY`, `Gst.State.PAUSED` and `Gst.State.PLAYING`. :type state: :class:`Gst.State` :rtype: :class:`True` if successfull, else :class:`False` """ @@ -717,7 +718,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('State change to %s: result=%s', state.value_name, result.value_name) - if result == Gst.STATE_CHANGE_FAILURE: + if result == Gst.State.CHANGE_FAILURE: logger.warning( 'Setting GStreamer state to %s failed', state.value_name) return False diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 8967a180..c77be700 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -58,7 +58,7 @@ class Scanner(object): duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: - pipeline.set_state(Gst.STATE_NULL) + pipeline.set_state(Gst.State.NULL) del pipeline return _Result(uri, tags, duration, seekable, mime, have_audio) @@ -110,8 +110,8 @@ def _pad_added(element, pad, pipeline): def _start_pipeline(pipeline): - if pipeline.set_state(Gst.STATE_PAUSED) == Gst.STATE_CHANGE_NO_PREROLL: - pipeline.set_state(Gst.STATE_PLAYING) + if pipeline.set_state(Gst.State.PAUSED) == Gst.State.CHANGE_NO_PREROLL: + pipeline.set_state(Gst.State.PLAYING) def _query_duration(pipeline): diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 48d3704b..ea5e5f25 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -520,17 +520,17 @@ class AudioStateTest(unittest.TestCase): def test_state_does_not_change_when_in_gst_ready_state(self): self.audio._handler.on_playbin_state_changed( - Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_VOID_PENDING) + Gst.State.NULL, Gst.State.READY, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) def test_state_changes_from_stopped_to_playing_on_play(self): self.audio._handler.on_playbin_state_changed( - Gst.STATE_NULL, Gst.STATE_READY, Gst.STATE_PLAYING) + Gst.State.NULL, Gst.State.READY, Gst.State.PLAYING) self.audio._handler.on_playbin_state_changed( - Gst.STATE_READY, Gst.STATE_PAUSED, Gst.STATE_PLAYING) + Gst.State.READY, Gst.State.PAUSED, Gst.State.PLAYING) self.audio._handler.on_playbin_state_changed( - Gst.STATE_PAUSED, Gst.STATE_PLAYING, Gst.STATE_VOID_PENDING) + Gst.State.PAUSED, Gst.State.PLAYING, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state) @@ -538,7 +538,7 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_VOID_PENDING) + Gst.State.PLAYING, Gst.State.PAUSED, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state) @@ -546,12 +546,12 @@ class AudioStateTest(unittest.TestCase): self.audio.state = audio.PlaybackState.PLAYING self.audio._handler.on_playbin_state_changed( - Gst.STATE_PLAYING, Gst.STATE_PAUSED, Gst.STATE_NULL) + Gst.State.PLAYING, Gst.State.PAUSED, Gst.State.NULL) self.audio._handler.on_playbin_state_changed( - Gst.STATE_PAUSED, Gst.STATE_READY, Gst.STATE_NULL) + Gst.State.PAUSED, Gst.State.READY, Gst.State.NULL) # We never get the following call, so the logic must work without it # self.audio._handler.on_playbin_state_changed( - # Gst.STATE_READY, Gst.STATE_NULL, Gst.STATE_VOID_PENDING) + # Gst.State.READY, Gst.State.NULL, Gst.State.VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) @@ -565,17 +565,17 @@ class AudioBufferingTest(unittest.TestCase): def test_pause_when_buffer_empty(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(Gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.State.PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) self.assertTrue(self.audio._buffering) def test_stay_paused_when_buffering_finished(self): playbin = self.audio._playbin self.audio.pause_playback() - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) playbin.set_state.reset_mock() self.audio._handler.on_buffering(100) @@ -585,11 +585,11 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_paused_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(Gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.State.PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) self.audio.pause_playback() playbin.set_state.reset_mock() @@ -600,13 +600,13 @@ class AudioBufferingTest(unittest.TestCase): def test_change_to_stopped_while_buffering(self): playbin = self.audio._playbin self.audio.start_playback() - playbin.set_state.assert_called_with(Gst.STATE_PLAYING) + playbin.set_state.assert_called_with(Gst.State.PLAYING) playbin.set_state.reset_mock() self.audio._handler.on_buffering(0) - playbin.set_state.assert_called_with(Gst.STATE_PAUSED) + playbin.set_state.assert_called_with(Gst.State.PAUSED) playbin.set_state.reset_mock() self.audio.stop_playback() - playbin.set_state.assert_called_with(Gst.STATE_NULL) + playbin.set_state.assert_called_with(Gst.State.NULL) self.assertFalse(self.audio._buffering) From 74cf32ede23890e521e5ea328e5b9e671f13498c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:37:54 +0200 Subject: [PATCH 07/92] gst1: Update SEEK_FLAG_* with SeekFlags.* --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bcd424bf..9f880982 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -633,7 +633,7 @@ class Audio(pykka.ThreadingActor): # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) result = self._playbin.seek_simple( - Gst.Format(Gst.FORMAT_TIME), Gst.SEEK_FLAG_FLUSH, gst_position) + Gst.Format(Gst.FORMAT_TIME), Gst.SeekFlags.FLUSH, gst_position) gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result From 5d6981d70ef03b8a27b5e8ca76d6d98b7277bdf8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:38:35 +0200 Subject: [PATCH 08/92] gst1: Update FORMAT_* with Format.* --- mopidy/audio/actor.py | 4 ++-- mopidy/audio/scan.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 9f880982..f338b377 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -614,7 +614,7 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - gst_position = self._playbin.query_position(Gst.FORMAT_TIME)[0] + gst_position = self._playbin.query_position(Gst.Format.TIME)[0] return utils.clocktime_to_millisecond(gst_position) except Gst.QueryError: # TODO: take state into account for this and possibly also return @@ -633,7 +633,7 @@ class Audio(pykka.ThreadingActor): # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) result = self._playbin.seek_simple( - Gst.Format(Gst.FORMAT_TIME), Gst.SeekFlags.FLUSH, gst_position) + Gst.Format.TIME, Gst.SeekFlags.FLUSH, gst_position) gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c77be700..bb778dc1 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -116,7 +116,7 @@ def _start_pipeline(pipeline): def _query_duration(pipeline): try: - duration = pipeline.query_duration(Gst.FORMAT_TIME, None)[0] + duration = pipeline.query_duration(Gst.Format.TIME, None)[0] except Gst.QueryError: return None @@ -127,7 +127,7 @@ def _query_duration(pipeline): def _query_seekable(pipeline): - query = Gst.query_new_seeking(Gst.FORMAT_TIME) + query = Gst.query_new_seeking(Gst.Format.TIME) pipeline.query(query) return query.parse_seeking()[1] From 8e771e89701f8c545d80def4d4cf5303486f83d5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:39:22 +0200 Subject: [PATCH 09/92] gst1: Update GhostPad() with GhostPad.new() --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index f338b377..8241f056 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -139,7 +139,7 @@ class _Outputs(Gst.Bin): self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) - ghost_pad = Gst.GhostPad('sink', self._tee.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', self._tee.get_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee @@ -487,7 +487,7 @@ class Audio(pykka.ThreadingActor): else: queue.link(self._outputs) - ghost_pad = Gst.GhostPad('sink', queue.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', queue.get_pad('sink')) audio_sink.add_pad(ghost_pad) self._playbin.set_property('audio-sink', audio_sink) From e402c9816c8cbb5bb4ed28b9b457008600ddf3cb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:45:26 +0200 Subject: [PATCH 10/92] gst1: Replace get_caps() with query_caps() --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index bb778dc1..389b7360 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -104,7 +104,7 @@ def _pad_added(element, pad, pipeline): sink.sync_state_with_parent() pad.link(sink.get_pad('sink')) - if pad.get_caps().is_subset(_RAW_AUDIO): + if pad.query_caps().is_subset(_RAW_AUDIO): struct = Gst.Structure('have-audio') element.get_bus().post(Gst.message_new_application(element, struct)) From 1cf450940a599295c6617d8db7ccf99234cdd14b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:48:35 +0200 Subject: [PATCH 11/92] gst1: Replace get_pad() with get_static_pad() --- mopidy/audio/actor.py | 7 ++++--- mopidy/audio/scan.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8241f056..8f45a7b5 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -139,7 +139,7 @@ class _Outputs(Gst.Bin): self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) - ghost_pad = Gst.GhostPad.new('sink', self._tee.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', self._tee.get_static_pad('sink')) self.add_pad(ghost_pad) # Add an always connected fakesink which respects the clock so the tee @@ -460,7 +460,8 @@ class Audio(pykka.ThreadingActor): except exceptions.AudioException: process.exit_process() # TODO: move this up the chain - self._handler.setup_event_handling(self._outputs.get_pad('sink')) + self._handler.setup_event_handling( + self._outputs.get_static_pad('sink')) def _setup_audio_sink(self): audio_sink = Gst.Bin('audio-sink') @@ -487,7 +488,7 @@ class Audio(pykka.ThreadingActor): else: queue.link(self._outputs) - ghost_pad = Gst.GhostPad.new('sink', queue.get_pad('sink')) + ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink')) audio_sink.add_pad(ghost_pad) self._playbin.set_property('audio-sink', audio_sink) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 389b7360..2880e67c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -102,7 +102,7 @@ def _pad_added(element, pad, pipeline): pipeline.add(sink) sink.sync_state_with_parent() - pad.link(sink.get_pad('sink')) + pad.link(sink.get_static_pad('sink')) if pad.query_caps().is_subset(_RAW_AUDIO): struct = Gst.Structure('have-audio') From 01bf8b773fcb9dd7d6135b4d8009c42ec3374fd2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:51:57 +0200 Subject: [PATCH 12/92] gst1: Replace buffer.timestamp with buffer.pts --- mopidy/audio/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index aa0b1d63..100654d6 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -31,7 +31,7 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): capabilites = Gst.caps_from_string(capabilites) buffer_.set_caps(capabilites) if timestamp: - buffer_.timestamp = timestamp + buffer_.pts = timestamp if duration: buffer_.duration = duration return buffer_ From 6c59205efe476254b2272f330df607e56b08d1ae Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:55:55 +0200 Subject: [PATCH 13/92] gst1: Replace 'struct[x] = y' with 'struct.set_value(x, y)' --- 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 2880e67c..780ca10a 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -91,8 +91,8 @@ def _setup_pipeline(uri, proxy_config=None): def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) - struct = Gst.Structure('have-type') - struct['caps'] = caps.get_structure(0) + struct = Gst.Structure.new_empty('have-type') + struct.set_value('caps', caps.get_structure(0)) element.get_bus().post(Gst.message_new_application(element, struct)) From aa3650bf34ce1e823cbf57860cb3c7868c59b71c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:57:12 +0200 Subject: [PATCH 14/92] gst1: Update query_new_duration() --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 780ca10a..3f221636 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -127,7 +127,7 @@ def _query_duration(pipeline): def _query_seekable(pipeline): - query = Gst.query_new_seeking(Gst.Format.TIME) + query = Gst.Query.new_seeking(Gst.Format.TIME) pipeline.query(query) return query.parse_seeking()[1] From c8ad7e3a414bb8452d7c4ee7c7f96fbd53b19865 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 00:59:50 +0200 Subject: [PATCH 15/92] gst1: Replace Caps() with Caps.from_string() And audio/x-raw-int and audio/x-raw-float with audio/x-raw --- mopidy/audio/actor.py | 2 +- mopidy/audio/scan.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8f45a7b5..d51519be 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -560,7 +560,7 @@ class Audio(pykka.ThreadingActor): :type seek_data: callable which takes time position in ms """ self._appsrc.prepare( - Gst.Caps(bytes(caps)), need_data, enough_data, seek_data) + Gst.Caps.from_string(caps), need_data, enough_data, seek_data) self._playbin.set_property('uri', 'appsrc://') def emit_data(self, buffer_): diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 3f221636..550b6c14 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -14,8 +14,6 @@ from mopidy.internal import encoding _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) -_RAW_AUDIO = Gst.Caps(b'audio/x-raw-int; audio/x-raw-float') - # TODO: replace with a scan(uri, timeout=1000, proxy_config=None)? class Scanner(object): @@ -104,7 +102,7 @@ def _pad_added(element, pad, pipeline): sink.sync_state_with_parent() pad.link(sink.get_static_pad('sink')) - if pad.query_caps().is_subset(_RAW_AUDIO): + if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): struct = Gst.Structure('have-audio') element.get_bus().post(Gst.message_new_application(element, struct)) From 63750d28fb1804438c295423be27df0b13ed91c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:02:12 +0200 Subject: [PATCH 16/92] gst1: Replace playbin2 with playbin --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index d51519be..92319525 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -257,7 +257,7 @@ class _Handler(object): # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() - if msg.structure.has_name('playbin2-stream-changed'): + if msg.structure.has_name('playbin-stream-changed'): self.on_stream_changed(msg.structure['uri']) return True @@ -427,7 +427,7 @@ class Audio(pykka.ThreadingActor): jacksink.set_rank(Gst.RANK_SECONDARY) def _setup_playbin(self): - playbin = Gst.ElementFactory.make('playbin2') + playbin = Gst.ElementFactory.make('playbin') playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO # TODO: turn into config values... From 2920f83065bd200615d923a2a74ca07459f63bab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:02:23 +0200 Subject: [PATCH 17/92] gst1: Replace decodebin2 with decodebin --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 550b6c14..c0db7583 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -70,7 +70,7 @@ def _setup_pipeline(uri, proxy_config=None): raise exceptions.ScannerError('GStreamer can not open: %s' % uri) typefind = Gst.ElementFactory.make('typefind') - decodebin = Gst.ElementFactory.make('decodebin2') + decodebin = Gst.ElementFactory.make('decodebin') pipeline = Gst.ElementFactory.make('pipeline') for e in (src, typefind, decodebin): From 1007d42dd16182f6dbd2d4494b1f04e55a06f331 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:06:47 +0200 Subject: [PATCH 18/92] gst1: GLib.get_system_config_dirs() now returns a list --- mopidy/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/commands.py b/mopidy/commands.py index 872d5773..af861032 100644 --- a/mopidy/commands.py +++ b/mopidy/commands.py @@ -19,7 +19,7 @@ from mopidy.internal import deps, process, timer, versioning logger = logging.getLogger(__name__) _default_config = [] -for base in GLib.get_system_config_dirs() + (GLib.get_user_config_dir(),): +for base in GLib.get_system_config_dirs() + [GLib.get_user_config_dir()]: _default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf')) DEFAULT_CONFIG = b':'.join(_default_config) From 8aad1d184605e4d2d6a53a046b314f3d42adab1e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:14:21 +0200 Subject: [PATCH 19/92] gst1: Replace registry_get_default() with registry.get() --- mopidy/audio/actor.py | 2 +- mopidy/audio/utils.py | 2 +- mopidy/internal/deps.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92319525..bee87b43 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -420,7 +420,7 @@ class Audio(pykka.ThreadingActor): def _setup_preferences(self): # TODO: move out of audio actor? # Fix for https://github.com/mopidy/mopidy/issues/604 - registry = Gst.registry_get_default() + registry = Gst.Registry.get() jacksink = registry.find_feature( 'jackaudiosink', Gst.TYPE_ELEMENT_FACTORY) if jacksink: diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 100654d6..5e8d3512 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -55,7 +55,7 @@ def supported_uri_schemes(uri_schemes): :rtype: set of URI schemes we can support via this GStreamer install. """ supported_schemes = set() - registry = Gst.registry_get_default() + registry = Gst.Registry.get() for factory in registry.get_feature_list(Gst.TYPE_ELEMENT_FACTORY): for uri in factory.get_uri_protocols(): diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 3744db87..c42f28fb 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -186,6 +186,6 @@ def _gstreamer_check_elements(): ] known_elements = [ factory.get_name() for factory in - Gst.registry_get_default().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] + Gst.Registry.get().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] return [ (element, element in known_elements) for element in elements_to_check] From a2b009c581e1e08c4af82cbad4e718115acd6cb3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:18:47 +0200 Subject: [PATCH 20/92] gst1: Replace TYPE_ELEMENT_FACTORY with ElementFactory --- mopidy/audio/actor.py | 3 +-- mopidy/audio/utils.py | 2 +- mopidy/internal/deps.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bee87b43..fe029500 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -421,8 +421,7 @@ class Audio(pykka.ThreadingActor): # TODO: move out of audio actor? # Fix for https://github.com/mopidy/mopidy/issues/604 registry = Gst.Registry.get() - jacksink = registry.find_feature( - 'jackaudiosink', Gst.TYPE_ELEMENT_FACTORY) + jacksink = registry.find_feature('jackaudiosink', Gst.ElementFactory) if jacksink: jacksink.set_rank(Gst.RANK_SECONDARY) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 5e8d3512..00f2c56a 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -57,7 +57,7 @@ def supported_uri_schemes(uri_schemes): supported_schemes = set() registry = Gst.Registry.get() - for factory in registry.get_feature_list(Gst.TYPE_ELEMENT_FACTORY): + for factory in registry.get_feature_list(Gst.ElementFactory): for uri in factory.get_uri_protocols(): if uri in uri_schemes: supported_schemes.add(uri) diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index c42f28fb..6c93a8fa 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -186,6 +186,6 @@ def _gstreamer_check_elements(): ] known_elements = [ factory.get_name() for factory in - Gst.Registry.get().get_feature_list(Gst.TYPE_ELEMENT_FACTORY)] + Gst.Registry.get().get_feature_list(Gst.ElementFactory)] return [ (element, element in known_elements) for element in elements_to_check] From 38bcdae1bf76dff338001f4c02904a7ac026c0e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:20:01 +0200 Subject: [PATCH 21/92] gst1: Replace RANK_SECONDARY with Rank.SECONDARY --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index fe029500..0abf9aa5 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -423,7 +423,7 @@ class Audio(pykka.ThreadingActor): registry = Gst.Registry.get() jacksink = registry.find_feature('jackaudiosink', Gst.ElementFactory) if jacksink: - jacksink.set_rank(Gst.RANK_SECONDARY) + jacksink.set_rank(Gst.Rank.SECONDARY) def _setup_playbin(self): playbin = Gst.ElementFactory.make('playbin') From 3f8ebc83c1a73491f33f93de64f05a3c43db9297 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:21:01 +0200 Subject: [PATCH 22/92] gst1: Replace ghost_unconnected_pads with ghost_unlinked_pads --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 0abf9aa5..8ecc0f37 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -152,7 +152,7 @@ class _Outputs(Gst.Bin): # XXX This only works for pipelines not in use until #790 gets done. try: output = Gst.parse_bin_from_description( - description, ghost_unconnected_pads=True) + description, ghost_unlinked_pads=True) except GObject.GError as ex: logger.error( 'Failed to create audio output "%s": %s', description, ex) From 9c0547d039fa51c2a7bf4653ce419a07a0193a87 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:23:50 +0200 Subject: [PATCH 23/92] gst1: Replace {add,remove}_event_probe() with {add,remove}_event() --- mopidy/audio/actor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 8ecc0f37..b6a441f4 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -219,7 +219,8 @@ class _Handler(object): def setup_event_handling(self, pad): self._pad = pad - self._event_handler_id = pad.add_event_probe(self.on_event) + self._event_handler_id = pad.add_probe( + Gst.PadProbeType.EVENT_BOTH, self.on_event) def teardown_message_handling(self): bus = self._element.get_bus() @@ -228,7 +229,7 @@ class _Handler(object): self._message_handler_id = None def teardown_event_handling(self): - self._pad.remove_event_probe(self._event_handler_id) + self._pad.remove_probe(self._event_handler_id) self._event_handler_id = None def on_message(self, bus, msg): From bd077591d0fff3bd2fc2bfd974db4ce076c14ba9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:25:27 +0200 Subject: [PATCH 24/92] gst1: Replace element_make_from_uri() with Element.make_from_uri() --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c0db7583..49b44f79 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -65,7 +65,7 @@ class Scanner(object): # Turns out it's _much_ faster to just create a new pipeline for every as # decodebins and other elements don't seem to take well to being reused. def _setup_pipeline(uri, proxy_config=None): - src = Gst.element_make_from_uri(Gst.URI_SRC, uri) + src = Gst.Element.make_from_uri(Gst.URI_SRC, uri) if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) From 3c2f83f6a6a9a378ea1dbcbce578659c89460fbf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:25:50 +0200 Subject: [PATCH 25/92] gst1: Replace Gst.URI_SRC with Gst.URIType.SRC --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 49b44f79..0518da8b 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -65,7 +65,7 @@ class Scanner(object): # Turns out it's _much_ faster to just create a new pipeline for every as # decodebins and other elements don't seem to take well to being reused. def _setup_pipeline(uri, proxy_config=None): - src = Gst.Element.make_from_uri(Gst.URI_SRC, uri) + src = Gst.Element.make_from_uri(Gst.URIType.SRC, uri) if not src: raise exceptions.ScannerError('GStreamer can not open: %s' % uri) From 67f4d57964f3df35c1d3644617c73b543f8d8626 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:31:28 +0200 Subject: [PATCH 26/92] gst1: Replace MESSAGE_* with MessageType.* --- mopidy/audio/actor.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index b6a441f4..e9d9f49c 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -233,21 +233,23 @@ class _Handler(object): self._event_handler_id = None def on_message(self, bus, msg): - if msg.type == Gst.MESSAGE_STATE_CHANGED and msg.src == self._element: + if ( + msg.type == Gst.MessageType.STATE_CHANGED and + msg.src == self._element): self.on_playbin_state_changed(*msg.parse_state_changed()) - elif msg.type == Gst.MESSAGE_BUFFERING: + elif msg.type == Gst.MessageType.BUFFERING: self.on_buffering(msg.parse_buffering(), msg.structure) - elif msg.type == Gst.MESSAGE_EOS: + elif msg.type == Gst.MessageType.EOS: self.on_end_of_stream() - elif msg.type == Gst.MESSAGE_ERROR: + elif msg.type == Gst.MessageType.ERROR: self.on_error(*msg.parse_error()) - elif msg.type == Gst.MESSAGE_WARNING: + elif msg.type == Gst.MessageType.WARNING: self.on_warning(*msg.parse_warning()) - elif msg.type == Gst.MESSAGE_ASYNC_DONE: + elif msg.type == Gst.MessageType.ASYNC_DONE: self.on_async_done() - elif msg.type == Gst.MESSAGE_TAG: + elif msg.type == Gst.MessageType.TAG: self.on_tag(msg.parse_tag()) - elif msg.type == Gst.MESSAGE_ELEMENT: + elif msg.type == Gst.MessageType.ELEMENT: if Gst.pbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) From e621d8055a011957582e195816ca74464eae60f4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:32:15 +0200 Subject: [PATCH 27/92] gst1: Replace gst.pbutils with GstPbutils --- mopidy/audio/actor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e9d9f49c..4d57e86e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -6,7 +6,7 @@ import threading import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import GObject, Gst, GstPbutils import pykka @@ -250,7 +250,7 @@ class _Handler(object): elif msg.type == Gst.MessageType.TAG: self.on_tag(msg.parse_tag()) elif msg.type == Gst.MessageType.ELEMENT: - if Gst.pbutils.is_missing_plugin_message(msg): + if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) def on_event(self, pad, event): @@ -347,12 +347,12 @@ class _Handler(object): AudioListener.send('tags_changed', tags=tags.keys()) def on_missing_plugin(self, msg): - desc = Gst.pbutils.missing_plugin_message_get_description(msg) - debug = Gst.pbutils.missing_plugin_message_get_installer_detail(msg) + desc = GstPbutils.missing_plugin_message_get_description(msg) + debug = GstPbutils.missing_plugin_message_get_installer_detail(msg) gst_logger.debug('Got missing-plugin message: description:%s', desc) logger.warning('Could not find a %s to handle media.', desc) - if Gst.pbutils.install_plugins_supported(): + if GstPbutils.install_plugins_supported(): logger.info('You might be able to fix this by running: ' 'gst-installer "%s"', debug) # TODO: store the missing plugins installer info in a file so we can From 1911ea0c103813ab03fcdc7075151ba3ccabe633 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:34:20 +0200 Subject: [PATCH 28/92] gst1: Replace STATE_CHANGE_* with StateChangeReturn.* --- mopidy/audio/actor.py | 2 +- mopidy/audio/scan.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 4d57e86e..92abe4bc 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -721,7 +721,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('State change to %s: result=%s', state.value_name, result.value_name) - if result == Gst.State.CHANGE_FAILURE: + if result == Gst.StateChangeReturn.FAILURE: logger.warning( 'Setting GStreamer state to %s failed', state.value_name) return False diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0518da8b..7cc5b1e5 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -108,7 +108,8 @@ def _pad_added(element, pad, pipeline): def _start_pipeline(pipeline): - if pipeline.set_state(Gst.State.PAUSED) == Gst.State.CHANGE_NO_PREROLL: + result = pipeline.set_state(Gst.State.PAUSED) + if result == Gst.StateChangeReturn.NO_PREROLL: pipeline.set_state(Gst.State.PLAYING) From 7c473eed070a1138855b99d7bbbb3eae37f565c5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:36:24 +0200 Subject: [PATCH 29/92] gst1: Replace MESSAGE_* with MessageType.* --- mopidy/audio/scan.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 7cc5b1e5..5f2d75b5 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -141,8 +141,13 @@ def _process(pipeline, timeout_ms): missing_message = None types = ( - Gst.MESSAGE_ELEMENT | Gst.MESSAGE_APPLICATION | Gst.MESSAGE_ERROR | - Gst.MESSAGE_EOS | Gst.MESSAGE_ASYNC_DONE | Gst.MESSAGE_TAG) + Gst.MessageType.ELEMENT | + Gst.MessageType.APPLICATION | + Gst.MessageType.ERROR | + Gst.MessageType.EOS | + Gst.MessageType.ASYNC_DONE | + Gst.MessageType.TAG + ) previous = clock.get_time() while timeout > 0: @@ -150,29 +155,29 @@ def _process(pipeline, timeout_ms): if message is None: break - elif message.type == Gst.MESSAGE_ELEMENT: + elif message.type == Gst.MessageType.ELEMENT: if GstPbutils.is_missing_plugin_message(message): missing_message = message - elif message.type == Gst.MESSAGE_APPLICATION: + elif message.type == Gst.MessageType.APPLICATION: if message.structure.get_name() == 'have-type': mime = message.structure['caps'].get_name() if mime.startswith('text/') or mime == 'application/xml': return tags, mime, have_audio elif message.structure.get_name() == 'have-audio': have_audio = True - elif message.type == Gst.MESSAGE_ERROR: + elif message.type == Gst.MessageType.ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_message and not mime: caps = missing_message.structure['detail'] mime = caps.get_structure(0).get_name() return tags, mime, have_audio raise exceptions.ScannerError(error) - elif message.type == Gst.MESSAGE_EOS: + elif message.type == Gst.MessageType.EOS: return tags, mime, have_audio - elif message.type == Gst.MESSAGE_ASYNC_DONE: + elif message.type == Gst.MessageType.ASYNC_DONE: if message.src == pipeline: return tags, mime, have_audio - elif message.type == Gst.MESSAGE_TAG: + elif message.type == Gst.MessageType.TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) From 1b47b6341e7dfa8a3ea96c211585c0fa47131423 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:37:31 +0200 Subject: [PATCH 30/92] gst1: Replace message_new_application() with Message.new_application() --- 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 5f2d75b5..0eb4067e 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -91,7 +91,7 @@ def _have_type(element, probability, caps, decodebin): decodebin.set_property('sink-caps', caps) struct = Gst.Structure.new_empty('have-type') struct.set_value('caps', caps.get_structure(0)) - element.get_bus().post(Gst.message_new_application(element, struct)) + element.get_bus().post(Gst.Message.new_application(element, struct)) def _pad_added(element, pad, pipeline): @@ -104,7 +104,7 @@ def _pad_added(element, pad, pipeline): if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): struct = Gst.Structure('have-audio') - element.get_bus().post(Gst.message_new_application(element, struct)) + element.get_bus().post(Gst.Message.new_application(element, struct)) def _start_pipeline(pipeline): From e6a4042c3e6ce21af9c3fa490d69b9d960e7d1e9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:47:52 +0200 Subject: [PATCH 31/92] gst1: Replace message.structure with message.get_structure() --- mopidy/audio/actor.py | 8 ++++---- mopidy/audio/scan.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92abe4bc..ea452c22 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -238,7 +238,7 @@ class _Handler(object): msg.src == self._element): self.on_playbin_state_changed(*msg.parse_state_changed()) elif msg.type == Gst.MessageType.BUFFERING: - self.on_buffering(msg.parse_buffering(), msg.structure) + self.on_buffering(msg.parse_buffering(), msg.get_structure()) elif msg.type == Gst.MessageType.EOS: self.on_end_of_stream() elif msg.type == Gst.MessageType.ERROR: @@ -260,8 +260,8 @@ class _Handler(object): # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() - if msg.structure.has_name('playbin-stream-changed'): - self.on_stream_changed(msg.structure['uri']) + if msg.get_structure().has_name('playbin-stream-changed'): + self.on_stream_changed(msg.get_structure().get_string('uri')) return True def on_playbin_state_changed(self, old_state, new_state, pending_state): @@ -303,7 +303,7 @@ class _Handler(object): def on_buffering(self, percent, structure=None): if structure and structure.has_field('buffering-mode'): - if structure['buffering-mode'] == Gst.BUFFERING_LIVE: + if structure.get_enum('buffering-mode') == Gst.BufferingMode.LIVE: return # Live sources stall in paused. level = logging.getLevelName('TRACE') diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0eb4067e..fbe80585 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -159,16 +159,16 @@ def _process(pipeline, timeout_ms): if GstPbutils.is_missing_plugin_message(message): missing_message = message elif message.type == Gst.MessageType.APPLICATION: - if message.structure.get_name() == 'have-type': - mime = message.structure['caps'].get_name() + if message.get_structure().get_name() == 'have-type': + mime = message.get_structure()['caps'].get_name() if mime.startswith('text/') or mime == 'application/xml': return tags, mime, have_audio - elif message.structure.get_name() == 'have-audio': + elif message.get_structure().get_name() == 'have-audio': have_audio = True elif message.type == Gst.MessageType.ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_message and not mime: - caps = missing_message.structure['detail'] + caps = missing_message.get_structure()['detail'] mime = caps.get_structure(0).get_name() return tags, mime, have_audio raise exceptions.ScannerError(error) From a0714455cd89b2eab9346c3d5d60ae942be492bf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 01:52:48 +0200 Subject: [PATCH 32/92] gst1: Use methods to get struct fields --- mopidy/audio/scan.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fbe80585..9d7ecde5 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -160,15 +160,16 @@ def _process(pipeline, timeout_ms): missing_message = message elif message.type == Gst.MessageType.APPLICATION: if message.get_structure().get_name() == 'have-type': - mime = message.get_structure()['caps'].get_name() - if mime.startswith('text/') or mime == 'application/xml': + mime = message.get_structure().get_value('caps').get_name() + if mime and ( + mime.startswith('text/') or mime == 'application/xml'): return tags, mime, have_audio elif message.get_structure().get_name() == 'have-audio': have_audio = True elif message.type == Gst.MessageType.ERROR: error = encoding.locale_decode(message.parse_error()[0]) if missing_message and not mime: - caps = missing_message.get_structure()['detail'] + caps = missing_message.get_structure().get_value('detail') mime = caps.get_structure(0).get_name() return tags, mime, have_audio raise exceptions.ScannerError(error) From f95e307ba06cda9d534db6dd37c6517942bd9055 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:05:59 +0200 Subject: [PATCH 33/92] gst1: Replace BUS_DROP with BusSyncReply.DROP --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ea452c22..ed118f8d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -690,7 +690,7 @@ class Audio(pykka.ThreadingActor): """ def sync_handler(bus, message): self._handler.on_message(bus, message) - return Gst.BUS_DROP + return Gst.BusSyncReply.DROP bus = self._playbin.get_bus() bus.set_sync_handler(sync_handler) From 275f9d50623d80fab3bcbb7860dd69e292da07b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:20:11 +0200 Subject: [PATCH 34/92] gst1: Buffers no longer have caps --- mopidy/audio/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 00f2c56a..3d9aad36 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -8,7 +8,7 @@ import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -from mopidy import compat, httpclient +from mopidy import httpclient from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) @@ -24,12 +24,11 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): """Create a new GStreamer buffer based on provided data. Mainly intended to keep gst imports out of non-audio modules. + + .. versionchanged:: 1.2 + ``capabilites`` argument is no longer in use """ - buffer_ = Gst.Buffer(data) - if capabilites: - if isinstance(capabilites, compat.string_types): - capabilites = Gst.caps_from_string(capabilites) - buffer_.set_caps(capabilites) + buffer_ = Gst.Buffer.new_wrapped(data) if timestamp: buffer_.pts = timestamp if duration: From 3d98a77a3c687d0ce429446c106aff675050cedb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:21:21 +0200 Subject: [PATCH 35/92] gst1: Replace FLOW_* with FlowReturn.* --- mopidy/audio/actor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ed118f8d..75c207dd 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -116,9 +116,11 @@ class _Appsrc(object): if buffer_ is None: gst_logger.debug('Sending appsrc end-of-stream event.') - return self._source.emit('end-of-stream') == Gst.FLOW_OK + result = self._source.emit('end-of-stream') + return result == Gst.FlowReturn.OK else: - return self._source.emit('push-buffer', buffer_) == Gst.FLOW_OK + result = self._source.emit('push-buffer', buffer_) + return result == Gst.FlowReturn.OK def _on_signal(self, element, clocktime, func): # This shim is used to ensure we always return true, and also handles From 8a846b860595d48aec5a5d0a1361f6a225f7327e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:30:36 +0200 Subject: [PATCH 36/92] gst1: Replace EVENT_* with EventType.* --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 75c207dd..92e6ceb2 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -256,9 +256,9 @@ class _Handler(object): self.on_missing_plugin(msg) def on_event(self, pad, event): - if event.type == Gst.EVENT_NEWSEGMENT: + if event.type == Gst.EventType.SEGMENT: self.on_new_segment(*event.parse_new_segment()) - elif event.type == Gst.EVENT_SINK_MESSAGE: + elif event.type == Gst.EventType.SINK_MESSAGE: # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. msg = event.parse_sink_message() From 6c9e2d4d3465470fb834870aadfcd76504e939b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 02:33:58 +0200 Subject: [PATCH 37/92] gst1: Add timeout to get_state() --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92e6ceb2..3d6284bc 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -683,7 +683,7 @@ class Audio(pykka.ThreadingActor): Should only be used by tests. """ - self._playbin.get_state() + self._playbin.get_state(timeout=1) def enable_sync_handler(self): """Enable manual processing of messages from bus. From ee51983cfdf72b632ba1e40ef54fc003b035fd5c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 2 Sep 2015 21:30:32 +0200 Subject: [PATCH 38/92] gst1: Replace TagList() with TagList.new_empty() --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 3d6284bc..510fcbba 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -743,7 +743,7 @@ class Audio(pykka.ThreadingActor): :param track: the current track :type track: :class:`mopidy.models.Track` """ - taglist = Gst.TagList() + taglist = Gst.TagList.new_empty() artists = [a for a in (track.artists or []) if a.name] # Default to blank data to trick shoutcast into clearing any previous From 3765e90bc744c3a43bd350738ee21afa3162fa0d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 22 Sep 2015 22:24:57 +0200 Subject: [PATCH 39/92] gst1: Replace DEBUG_BIN_TO_DOT_FILE with debug_bin_to_dot_file --- mopidy/audio/actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 510fcbba..94fa5aea 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -300,8 +300,8 @@ class _Handler(object): AudioListener.send('stream_changed', uri=None) if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ: - Gst.DEBUG_BIN_TO_DOT_FILE( - self._audio._playbin, Gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') + Gst.debug_bin_to_dot_file( + self._audio._playbin, Gst.DebugGraphDetails.ALL, 'mopidy') def on_buffering(self, percent, structure=None): if structure and structure.has_field('buffering-mode'): From ef40854b8629e88fe591f7801828876833a2cb0a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 21:32:57 +0100 Subject: [PATCH 40/92] gst1: Update index into query_position() result --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 94fa5aea..7cbe9393 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -619,7 +619,7 @@ class Audio(pykka.ThreadingActor): :rtype: int """ try: - gst_position = self._playbin.query_position(Gst.Format.TIME)[0] + gst_position = self._playbin.query_position(Gst.Format.TIME)[1] return utils.clocktime_to_millisecond(gst_position) except Gst.QueryError: # TODO: take state into account for this and possibly also return From ee99bedf3919506653ae71a2cc9b4074cd34de2a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 21:39:09 +0100 Subject: [PATCH 41/92] gst1: Gst.Bin() no longer takes a name --- mopidy/audio/actor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 7cbe9393..c51449c5 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -136,7 +136,8 @@ class _Appsrc(object): class _Outputs(Gst.Bin): def __init__(self): - Gst.Bin.__init__(self, 'outputs') + Gst.Bin.__init__(self) + # TODO gst1: Set 'outputs' as the Bin name for easier debugging self._tee = Gst.ElementFactory.make('tee') self.add(self._tee) @@ -468,7 +469,7 @@ class Audio(pykka.ThreadingActor): self._outputs.get_static_pad('sink')) def _setup_audio_sink(self): - audio_sink = Gst.Bin('audio-sink') + audio_sink = Gst.ElementFactory.make('bin', 'audio-sink') # Queue element to buy us time between the about to finish event and # the actual switch, i.e. about to switch can block for longer thanks From 5277ad5ff573bd0d4b1256bd1c3d022fcc4c3877 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 21:54:01 +0100 Subject: [PATCH 42/92] gst1: Update get_enum() to include enum type it expects --- mopidy/audio/actor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index c51449c5..e2ee4041 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -305,8 +305,10 @@ class _Handler(object): self._audio._playbin, Gst.DebugGraphDetails.ALL, 'mopidy') def on_buffering(self, percent, structure=None): - if structure and structure.has_field('buffering-mode'): - if structure.get_enum('buffering-mode') == Gst.BufferingMode.LIVE: + if structure is not None and structure.has_field('buffering-mode'): + buffering_mode = structure.get_enum( + 'buffering-mode', Gst.BufferingMode) + if buffering_mode == Gst.BufferingMode.LIVE: return # Live sources stall in paused. level = logging.getLevelName('TRACE') From 87b1c9455c89b9dbd245d5851d55659e2b2621b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 23:37:22 +0100 Subject: [PATCH 43/92] gst1: Update query_duration() usage --- mopidy/audio/scan.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 9d7ecde5..c51762b9 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -114,15 +114,12 @@ def _start_pipeline(pipeline): def _query_duration(pipeline): - try: - duration = pipeline.query_duration(Gst.Format.TIME, None)[0] - except Gst.QueryError: + success, duration = pipeline.query_duration(Gst.Format.TIME) + + if not success or duration < 0: return None - if duration < 0: - return None - else: - return duration // Gst.MSECOND + return duration // Gst.MSECOND def _query_seekable(pipeline): From 01cf013b098964b1fd7ddd3efb80d27c4dbc4bcd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 23:39:12 +0100 Subject: [PATCH 44/92] gst1: Update query_position() usage --- mopidy/audio/actor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e2ee4041..a2401ea9 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -621,15 +621,16 @@ class Audio(pykka.ThreadingActor): :rtype: int """ - try: - gst_position = self._playbin.query_position(Gst.Format.TIME)[1] - return utils.clocktime_to_millisecond(gst_position) - except Gst.QueryError: + success, position = self._playbin.query_position(Gst.Format.TIME) + + if not success: # TODO: take state into account for this and possibly also return # None as the unknown value instead of zero? logger.debug('Position query failed') return 0 + return utils.clocktime_to_millisecond(position) + def set_position(self, position): """ Set position in milliseconds. From 20b1c21b0b5847f2c0bb33eb050e3fc3020b83a6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 26 Oct 2015 23:52:17 +0100 Subject: [PATCH 45/92] gst1: Avoid using pipeline.get_clock() Often the clock isn't available for use. gst_pipeline_clock() which is always available requires Gst 1.6. --- mopidy/audio/scan.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c51762b9..718f2d6e 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -2,6 +2,7 @@ from __future__ import ( absolute_import, division, print_function, unicode_literals) import collections +import time import gi gi.require_version('Gst', '1.0') @@ -129,9 +130,7 @@ def _query_seekable(pipeline): def _process(pipeline, timeout_ms): - clock = pipeline.get_clock() bus = pipeline.get_bus() - timeout = timeout_ms * Gst.MSECOND tags = {} mime = None have_audio = False @@ -146,9 +145,10 @@ def _process(pipeline, timeout_ms): Gst.MessageType.TAG ) - previous = clock.get_time() + timeout = timeout_ms + previous = int(time.time() * 1000) while timeout > 0: - message = bus.timed_pop_filtered(timeout, types) + message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types) if message is None: break @@ -180,7 +180,7 @@ def _process(pipeline, timeout_ms): # Note that this will only keep the last tag. tags.update(utils.convert_taglist(taglist)) - now = clock.get_time() + now = int(time.time() * 1000) timeout -= now - previous previous = now From fc54a17b44b903f7d46426b6e9bf892b2443faed Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 21:29:09 +0100 Subject: [PATCH 46/92] gst1: require_version('Gst', '1.0') before use --- mopidy/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index c91740a3..6d399bd4 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -7,6 +7,8 @@ import sys import textwrap try: + import gi + gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst except ImportError: print(textwrap.dedent(""" From da19c8be56320fa92e7e4c9c04f92b701d5ce884 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 21:45:10 +0100 Subject: [PATCH 47/92] gst1: on_new_segment() gets a Segment struct --- mopidy/audio/actor.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index a2401ea9..5506475d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -258,7 +258,7 @@ class _Handler(object): def on_event(self, pad, event): if event.type == Gst.EventType.SEGMENT: - self.on_new_segment(*event.parse_new_segment()) + self.on_new_segment(event.parse_new_segment()) elif event.type == Gst.EventType.SINK_MESSAGE: # Handle stream changed messages when they reach our output bin. # If we listen for it on the bus we get one per tee branch. @@ -364,11 +364,18 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? - def on_new_segment(self, update, rate, format_, start, stop, position): - gst_logger.debug('Got new-segment event: update=%s rate=%s format=%s ' - 'start=%s stop=%s position=%s', update, rate, - format_.value_name, start, stop, position) - position_ms = position // Gst.MSECOND + def on_new_segment(self, segment): + gst_logger.debug( + 'Got new-segment event: ' + 'rate=%(rate)s format=%(format)s start=%(start)s stop=%(stop)s ' + 'position=%(position)s', { + 'rate': segment.rate, + 'format': Gst.Format.get_name(segment.format), + 'start': segment.start, + 'stop': segment.stop, + 'position': segment.position + }) + position_ms = segment.position // Gst.MSECOND logger.debug('Audio event: position_changed(position=%s)', position_ms) AudioListener.send('position_changed', position=position_ms) From 3792b8c9006b91ef61fd6da5dfb1e482857108c8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 22:00:59 +0100 Subject: [PATCH 48/92] gst1: Use Gst.CLOCK_TIME_NONE to block for state changes in tests --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5506475d..5beb840a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -694,7 +694,7 @@ class Audio(pykka.ThreadingActor): Should only be used by tests. """ - self._playbin.get_state(timeout=1) + self._playbin.get_state(timeout=Gst.CLOCK_TIME_NONE) def enable_sync_handler(self): """Enable manual processing of messages from bus. From 13567d271a30aa483aee9ecf60abf4d79c96118a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 27 Oct 2015 22:39:08 +0100 Subject: [PATCH 49/92] gst1: Update taglist translator --- mopidy/audio/utils.py | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 3d9aad36..cc312b73 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -import datetime +import collections import logging import numbers @@ -8,7 +8,7 @@ import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -from mopidy import httpclient +from mopidy import compat, httpclient from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) @@ -154,7 +154,7 @@ def setup_proxy(element, config): def convert_taglist(taglist): - """Convert a :class:`Gst.Taglist` to plain Python types. + """Convert a :class:`Gst.TagList` to plain Python types. Knows how to convert: @@ -167,37 +167,26 @@ def convert_taglist(taglist): Unknown types will be ignored and debug logged. Tag keys are all strings defined as part GStreamer under GstTagList_. - .. _GstTagList: http://gstreamer.freedesktop.org/data/doc/gstreamer/\ -0.10.36/gstreamer/html/gstreamer-GstTagList.html + .. _GstTagList: https://developer.gnome.org/gstreamer/stable/\ +gstreamer-GstTagList.html :param taglist: A GStreamer taglist to be converted. - :type taglist: :class:`Gst.Taglist` + :type taglist: :class:`Gst.TagList` :rtype: dictionary of tag keys with a list of values. """ - result = {} + result = collections.defaultdict(list) - # Taglists are not really dicts, hence the lack of .items() and - # explicit use of .keys() - for key in taglist.keys(): - result.setdefault(key, []) + for n in range(taglist.n_tags()): + tag = taglist.nth_tag_name(n) - values = taglist[key] - if not isinstance(values, list): - values = [values] + for i in range(taglist.get_tag_size(tag)): + value = taglist.get_value_index(tag, i) - for value in values: - if isinstance(value, Gst.Date): - try: - date = datetime.date(value.year, value.month, value.day) - result[key].append(date) - except ValueError: - logger.debug('Ignoring invalid date: %r = %r', key, value) - elif isinstance(value, Gst.Buffer): - result[key].append(bytes(value)) - elif isinstance( - value, (compat.string_types, bool, numbers.Number)): - result[key].append(value) + if isinstance(value, Gst.DateTime): + result[tag].append(value.to_iso8601_string()) + if isinstance(value, (compat.string_types, bool, numbers.Number)): + result[tag].append(value) else: - logger.debug('Ignoring unknown data: %r = %r', key, value) + logger.debug('Ignoring unknown tag data: %r = %r', tag, value) return result From 3e4bd16be2b648901719ee27da859bd749be31fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 23 Nov 2015 00:10:39 +0100 Subject: [PATCH 50/92] gst1: Replace playbin-stream-changed with Gst.MessageType.STREAM_START --- mopidy/audio/actor.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 5beb840a..e6dca996 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -255,16 +255,12 @@ class _Handler(object): elif msg.type == Gst.MessageType.ELEMENT: if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) + elif msg.type == Gst.MessageType.STREAM_START: + self.on_stream_changed(self._audio._playbin.get_property('uri')) def on_event(self, pad, event): if event.type == Gst.EventType.SEGMENT: self.on_new_segment(event.parse_new_segment()) - elif event.type == Gst.EventType.SINK_MESSAGE: - # Handle stream changed messages when they reach our output bin. - # If we listen for it on the bus we get one per tee branch. - msg = event.parse_sink_message() - if msg.get_structure().has_name('playbin-stream-changed'): - self.on_stream_changed(msg.get_structure().get_string('uri')) return True def on_playbin_state_changed(self, old_state, new_state, pending_state): From 29a194cb551196f790a2949c52654a1218628e23 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 23 Nov 2015 21:25:20 +0100 Subject: [PATCH 51/92] gst1: Use new API for TagList creation --- mopidy/audio/actor.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index e6dca996..4de7f833 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -743,7 +743,7 @@ class Audio(pykka.ThreadingActor): """ Set track metadata for currently playing song. - Only needs to be called by sources such as `appsrc` which do not + Only needs to be called by sources such as ``appsrc`` which do not already inject tags in playbin, e.g. when using :meth:`emit_data` to deliver raw audio data to GStreamer. @@ -753,20 +753,27 @@ class Audio(pykka.ThreadingActor): taglist = Gst.TagList.new_empty() artists = [a for a in (track.artists or []) if a.name] + def set_value(tag, value): + gobject_value = GObject.Value() + gobject_value.init(GObject.TYPE_STRING) + gobject_value.set_string(value) + taglist.add_value( + Gst.TagMergeMode.REPLACE, Gst.TAG_ARTIST, gobject_value) + # Default to blank data to trick shoutcast into clearing any previous # values it might have. - taglist[Gst.TAG_ARTIST] = ' ' - taglist[Gst.TAG_TITLE] = ' ' - taglist[Gst.TAG_ALBUM] = ' ' + set_value(Gst.TAG_ARTIST, ' ') + set_value(Gst.TAG_TITLE, ' ') + set_value(Gst.TAG_ALBUM, ' ') if artists: - taglist[Gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) + set_value(Gst.TAG_ARTIST, ', '.join([a.name for a in artists])) if track.name: - taglist[Gst.TAG_TITLE] = track.name + set_value(Gst.TAG_TITLE, track.name) if track.album and track.album.name: - taglist[Gst.TAG_ALBUM] = track.album.name + set_value(Gst.TAG_ALBUM, track.album.name) event = Gst.event_new_tag(taglist) # TODO: check if we get this back on our own bus? From ce198ba9f83cf4ee2467025a8f99f459c66c7cef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 27 Nov 2015 15:07:16 +0100 Subject: [PATCH 52/92] gst1: Update pad probe callback to match new signature --- mopidy/audio/actor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 4de7f833..80462767 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -223,7 +223,7 @@ class _Handler(object): def setup_event_handling(self, pad): self._pad = pad self._event_handler_id = pad.add_probe( - Gst.PadProbeType.EVENT_BOTH, self.on_event) + Gst.PadProbeType.EVENT_BOTH, self.on_pad_event) def teardown_message_handling(self): bus = self._element.get_bus() @@ -258,10 +258,11 @@ class _Handler(object): elif msg.type == Gst.MessageType.STREAM_START: self.on_stream_changed(self._audio._playbin.get_property('uri')) - def on_event(self, pad, event): + def on_pad_event(self, pad, pad_probe_info): + event = pad_probe_info.get_event() if event.type == Gst.EventType.SEGMENT: self.on_new_segment(event.parse_new_segment()) - return True + return Gst.PadProbeReturn.OK def on_playbin_state_changed(self, old_state, new_state, pending_state): gst_logger.debug('Got state-changed message: old=%s new=%s pending=%s', From 592f5dec53caa0797b80180b240a32ccceb41f90 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 11:46:29 +0100 Subject: [PATCH 53/92] gst1: Remove deprecated GObject.threads_init() Ref https://wiki.gnome.org/Projects/PyGObject/Threading "The requirement to call GObject.threads_init() has been removed from PyGObject 3.10.2 when using Python native threads with GI (via the threading module) as well as with GI repositories which manage their own threads that may call back into Python (like GStreamer callbacks). The GObject.threads_init() function will still exist for the entire 3.x series for compatibility reasons but emits a deprecation warning." --- mopidy/__main__.py | 11 +++++------ mopidy/audio/scan.py | 3 +-- tests/audio/test_actor.py | 3 +-- tests/audio/test_scan.py | 3 +-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 6d399bd4..06b7658d 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -9,21 +9,20 @@ import textwrap try: import gi gi.require_version('Gst', '1.0') - from gi.repository import GObject, Gst + from gi.repository import Gst except ImportError: print(textwrap.dedent(""" - ERROR: The GObject and Gst Python packages were not found. + ERROR: The GStreamer Python package was not found. - Mopidy requires GStreamer and GObject to work. These are C libraries - with a number of dependencies themselves, and cannot be installed with - the regular Python tools like pip. + Mopidy requires GStreamer to work. GStreamer is a C library with a + number of dependencies itself, and cannot be installed with the regular + Python tools like pip. Please see http://docs.mopidy.com/en/latest/installation/ for instructions on how to install the required dependencies. """)) raise -GObject.threads_init() Gst.init() try: diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 718f2d6e..fdd97784 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -6,7 +6,7 @@ import time import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst, GstPbutils +from gi.repository import Gst, GstPbutils from mopidy import exceptions from mopidy.audio import utils @@ -193,7 +193,6 @@ if __name__ == '__main__': from mopidy.internal import path - GObject.threads_init() Gst.init() scanner = Scanner(5000) diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index ea5e5f25..0cf89418 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -5,9 +5,8 @@ import unittest import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import Gst -GObject.threads_init() Gst.init() import mock diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index ab995285..6e3ba001 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -5,9 +5,8 @@ import unittest import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import Gst -GObject.threads_init() Gst.init() from mopidy import exceptions From eb4c742015612a0f9a39a64657fff4d6a8d981f7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 30 Nov 2015 15:27:25 +0100 Subject: [PATCH 54/92] gst1: Run gst.init() if needed everywhere using Gst --- mopidy/__main__.py | 4 ++-- mopidy/audio/actor.py | 1 + mopidy/audio/scan.py | 3 +-- mopidy/internal/deps.py | 1 + tests/audio/test_actor.py | 2 -- tests/audio/test_scan.py | 6 ------ 6 files changed, 5 insertions(+), 12 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 06b7658d..1d9e8314 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -22,8 +22,8 @@ except ImportError: instructions on how to install the required dependencies. """)) raise - -Gst.init() +else: + Gst.init() try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 80462767..c5de90dc 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -7,6 +7,7 @@ import threading import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst, GstPbutils +Gst.is_initialized() or Gst.init() import pykka diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fdd97784..f4bbd3a0 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -7,6 +7,7 @@ import time import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GstPbutils +Gst.is_initialized() or Gst.init() from mopidy import exceptions from mopidy.audio import utils @@ -193,8 +194,6 @@ if __name__ == '__main__': from mopidy.internal import path - Gst.init() - scanner = Scanner(5000) for uri in sys.argv[1:]: if not Gst.uri_is_valid(uri): diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 6c93a8fa..8947025f 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -8,6 +8,7 @@ import sys import gi gi.require_version('Gst', '1.0') from gi.repository import Gst +Gst.is_initialized() or Gst.init() import pkg_resources diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 0cf89418..41f730e8 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -7,8 +7,6 @@ import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -Gst.init() - import mock import pykka diff --git a/tests/audio/test_scan.py b/tests/audio/test_scan.py index 6e3ba001..411ce805 100644 --- a/tests/audio/test_scan.py +++ b/tests/audio/test_scan.py @@ -3,12 +3,6 @@ from __future__ import absolute_import, unicode_literals import os import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - -Gst.init() - from mopidy import exceptions from mopidy.audio import scan from mopidy.internal import path as path_lib From 812e53b8953c6690d0f774580c6bc7eedb991dc3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 30 Nov 2015 15:29:32 +0100 Subject: [PATCH 55/92] gst1: Replace parse_new_segment() with parse_segment() Fixes 4 unit tests --- mopidy/audio/actor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index c5de90dc..811b1ae6 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -262,7 +262,7 @@ class _Handler(object): def on_pad_event(self, pad, pad_probe_info): event = pad_probe_info.get_event() if event.type == Gst.EventType.SEGMENT: - self.on_new_segment(event.parse_new_segment()) + self.on_segment(event.parse_segment()) return Gst.PadProbeReturn.OK def on_playbin_state_changed(self, old_state, new_state, pending_state): @@ -362,9 +362,9 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? - def on_new_segment(self, segment): + def on_segment(self, segment): gst_logger.debug( - 'Got new-segment event: ' + 'Got SEGMENT pad event: ' 'rate=%(rate)s format=%(format)s start=%(start)s stop=%(stop)s ' 'position=%(position)s', { 'rate': segment.rate, From 226c937ffc74e4cb2e83375afa6b0ed4b33bde3d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 00:04:10 +0100 Subject: [PATCH 56/92] gst1: Tune log messages --- mopidy/audio/actor.py | 88 ++++++++++++++++++++++++----------------- mopidy/core/playback.py | 2 +- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 811b1ae6..10073121 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -20,9 +20,9 @@ from mopidy.internal import deprecation, process logger = logging.getLogger(__name__) -# This logger is only meant for debug logging of low level gstreamer info such +# This logger is only meant for debug logging of low level GStreamer info such # as callbacks, event, messages and direct interaction with GStreamer such as -# set_state on a pipeline. +# set_state() on a pipeline. gst_logger = logging.getLogger('mopidy.audio.gst') _GST_STATE_MAPPING = { @@ -237,22 +237,26 @@ class _Handler(object): self._event_handler_id = None def on_message(self, bus, msg): - if ( - msg.type == Gst.MessageType.STATE_CHANGED and - msg.src == self._element): - self.on_playbin_state_changed(*msg.parse_state_changed()) + if msg.type == Gst.MessageType.STATE_CHANGED: + if msg.src != self._element: + return + old_state, new_state, pending_state = msg.parse_state_changed() + self.on_playbin_state_changed(old_state, new_state, pending_state) elif msg.type == Gst.MessageType.BUFFERING: self.on_buffering(msg.parse_buffering(), msg.get_structure()) elif msg.type == Gst.MessageType.EOS: self.on_end_of_stream() elif msg.type == Gst.MessageType.ERROR: - self.on_error(*msg.parse_error()) + error, debug = msg.parse_error() + self.on_error(error, debug) elif msg.type == Gst.MessageType.WARNING: - self.on_warning(*msg.parse_warning()) + error, debug = msg.parse_warning() + self.on_warning(error, debug) elif msg.type == Gst.MessageType.ASYNC_DONE: self.on_async_done() elif msg.type == Gst.MessageType.TAG: - self.on_tag(msg.parse_tag()) + taglist = msg.parse_tag() + self.on_tag(taglist) elif msg.type == Gst.MessageType.ELEMENT: if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) @@ -266,14 +270,16 @@ class _Handler(object): return Gst.PadProbeReturn.OK def on_playbin_state_changed(self, old_state, new_state, pending_state): - gst_logger.debug('Got state-changed message: old=%s new=%s pending=%s', - old_state.value_name, new_state.value_name, - pending_state.value_name) + gst_logger.debug( + 'Got STATE_CHANGED bus message: old=%s new=%s pending=%s', + old_state.value_name, new_state.value_name, + pending_state.value_name) if new_state == Gst.State.READY and pending_state == Gst.State.NULL: # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. + # TODO/Gst1: Is this workaround still needed? new_state = Gst.State.NULL pending_state = Gst.State.VOID_PENDING @@ -320,31 +326,37 @@ class _Handler(object): self._audio._playbin.set_state(Gst.State.PLAYING) level = logging.DEBUG - gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) + gst_logger.log( + level, 'Got BUFFERING bus message: percent=%d%%', percent) def on_end_of_stream(self): - gst_logger.debug('Got end-of-stream message.') + gst_logger.debug('Got EOS (end of stream) bus message.') logger.debug('Audio event: reached_end_of_stream()') self._audio._tags = {} AudioListener.send('reached_end_of_stream') def on_error(self, error, debug): - gst_logger.error(str(error).decode('utf-8')) - if debug: - gst_logger.debug(debug.decode('utf-8')) + error_msg = str(error).decode('utf-8') + debug_msg = debug.decode('utf-8') + gst_logger.debug( + 'Got ERROR bus message: error=%r debug=%r', error_msg, debug_msg) + gst_logger.error('GStreamer error: %s', error_msg) # TODO: is this needed? self._audio.stop_playback() def on_warning(self, error, debug): - gst_logger.warning(str(error).decode('utf-8')) - if debug: - gst_logger.debug(debug.decode('utf-8')) + error_msg = str(error).decode('utf-8') + debug_msg = debug.decode('utf-8') + gst_logger.warning('GStreamer warning: %s', error_msg) + gst_logger.debug( + 'Got WARNING bus message: error=%r debug=%r', error_msg, debug_msg) def on_async_done(self): - gst_logger.debug('Got async-done.') + gst_logger.debug('Got ASYNC_DONE bus message.') def on_tag(self, taglist): tags = utils.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()) @@ -352,8 +364,8 @@ class _Handler(object): def on_missing_plugin(self, msg): desc = GstPbutils.missing_plugin_message_get_description(msg) debug = GstPbutils.missing_plugin_message_get_installer_detail(msg) - - gst_logger.debug('Got missing-plugin message: description:%s', desc) + gst_logger.debug( + 'Got missing-plugin bus message: description=%r', desc) logger.warning('Could not find a %s to handle media.', desc) if GstPbutils.install_plugins_supported(): logger.info('You might be able to fix this by running: ' @@ -362,6 +374,11 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? + def on_stream_changed(self, uri): + gst_logger.debug('Got STREAM_CHANGED bus message: uri=%r', uri) + logger.debug('Audio event: stream_changed(uri=%r)', uri) + AudioListener.send('stream_changed', uri=uri) + def on_segment(self, segment): gst_logger.debug( 'Got SEGMENT pad event: ' @@ -374,14 +391,9 @@ class _Handler(object): 'position': segment.position }) position_ms = segment.position // Gst.MSECOND - logger.debug('Audio event: position_changed(position=%s)', position_ms) + logger.debug('Audio event: position_changed(position=%r)', position_ms) AudioListener.send('position_changed', position=position_ms) - def on_stream_changed(self, uri): - gst_logger.debug('Got stream-changed message: uri=%s', uri) - logger.debug('Audio event: stream_changed(uri=%s)', uri) - AudioListener.send('stream_changed', uri=uri) - # TODO: create a player class which replaces the actors internals class Audio(pykka.ThreadingActor): @@ -478,7 +490,7 @@ class Audio(pykka.ThreadingActor): def _setup_audio_sink(self): audio_sink = Gst.ElementFactory.make('bin', 'audio-sink') - # Queue element to buy us time between the about to finish event and + # Queue element to buy us time between the about-to-finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. # TODO: make the min-max values a setting? @@ -517,11 +529,12 @@ class Audio(pykka.ThreadingActor): gst_logger.debug('Got about-to-finish event.') if self._about_to_finish_callback: - logger.debug('Running about to finish callback.') + logger.debug('Running about-to-finish callback.') self._about_to_finish_callback() def _on_source_setup(self, element, source): - gst_logger.debug('Got source-setup: element=%s', source) + gst_logger.debug( + 'Got source-setup signal: element=%s', source.__class__.__name__) if source.get_factory().get_name() == 'appsrc': self._appsrc.configure(source) @@ -646,9 +659,9 @@ class Audio(pykka.ThreadingActor): """ # TODO: double check seek flags in use. gst_position = utils.millisecond_to_clocktime(position) + gst_logger.debug('Sending flushing seek: position=%r', gst_position) result = self._playbin.seek_simple( Gst.Format.TIME, Gst.SeekFlags.FLUSH, gst_position) - gst_logger.debug('Sent flushing seek: position=%s', gst_position) return result def start_playback(self): @@ -729,8 +742,9 @@ class Audio(pykka.ThreadingActor): """ self._target_state = state result = self._playbin.set_state(state) - gst_logger.debug('State change to %s: result=%s', state.value_name, - result.value_name) + gst_logger.debug( + 'Changing state to %s: result=%s', state.value_name, + result.value_name) if result == Gst.StateChangeReturn.FAILURE: logger.warning( @@ -777,10 +791,12 @@ 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) - gst_logger.debug('Sent tag event: track=%s', track.uri) def get_current_tags(self): """ diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py index 7170969e..63259f7d 100644 --- a/mopidy/core/playback.py +++ b/mopidy/core/playback.py @@ -459,7 +459,7 @@ class PlaybackController(object): if time_position < 0: time_position = 0 elif time_position > tl_track.track.length: - # TODO: gstreamer will trigger a about to finish for us, use that? + # TODO: GStreamer will trigger a about-to-finish for us, use that? self.next() return True From 7a3d5ff13ce9928ab02f99dab6903435c51968d3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 00:05:00 +0100 Subject: [PATCH 57/92] gst1: Replace event_new_tag() with Event.new_tag() --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 10073121..40e32992 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -794,7 +794,7 @@ class Audio(pykka.ThreadingActor): gst_logger.debug( 'Sending TAG event for track %r: %r', track.uri, taglist.to_string()) - event = Gst.event_new_tag(taglist) + event = Gst.Event.new_tag(taglist) # TODO: check if we get this back on our own bus? self._playbin.send_event(event) From 780c493af36f1416ba7c89c335999938391fa0b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 00:13:56 +0100 Subject: [PATCH 58/92] gst1: Replace Structure(...) with Stricture.new_empty(...) --- mopidy/audio/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f4bbd3a0..fb0773d6 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -105,7 +105,7 @@ def _pad_added(element, pad, pipeline): pad.link(sink.get_static_pad('sink')) if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): - struct = Gst.Structure('have-audio') + struct = Gst.Structure.new_empty('have-audio') element.get_bus().post(Gst.Message.new_application(element, struct)) From 0ef3da5ed328f8657793ed25bd743ac723e4053a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 02:02:23 +0100 Subject: [PATCH 59/92] travis: Replace GStreamer 0.10 with 1.x --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 964ae89f..2acbf87e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: before_install: - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573 - "sudo apt-get update -qq" - - "sudo apt-get install -y graphviz-dev gstreamer0.10-plugins-good python-gst0.10" + - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good python-gst-1.0" install: - "pip install tox" From dd466ed89549dadf7a6f649c631f465618d83150 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 02:35:00 +0100 Subject: [PATCH 60/92] docs: Update GStreamer install docs --- docs/installation/source.rst | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index 204cc1df..d9994c6b 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -37,36 +37,33 @@ please follow the directions :ref:`here `. On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the following steps. -#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python - bindings. GStreamer is packaged for most popular Linux distributions. Search - for GStreamer in your package manager, and make sure to install the Python +#. Then you'll need to install GStreamer 1.x (>= 1.2), with Python bindings. + GStreamer is packaged for most popular Linux distributions. Search for + GStreamer in your package manager, and make sure to install the Python bindings, and the "good" and "ugly" plugin sets. If you use Debian/Ubuntu you can install GStreamer like this:: - sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ - gstreamer0.10-plugins-ugly gstreamer0.10-tools + sudo apt-get install python-gst-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:: - sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \ - gstreamer0.10-ugly-plugins + sudo pacman -S gst-python gst-plugins-good gst-plugins-ugly If you use Fedora you can install GStreamer like this:: - sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \ - gstreamer0.10-plugins-ugly gstreamer0.10-tools + # TODO Update to GStreamer 1 + sudo yum install -y python-gstreamer1 gstreamer1-plugins-good \ + gstreamer1-plugins-ugly - If you use Gentoo you need to be careful because GStreamer 0.10 is in a - different lower slot than 1.0, the default. Your emerge commands will need - to include the slot:: + If you use Gentoo you can install GStreamer like this:: - emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \ - gst-plugins-ugly:0.10 gst-plugins-meta:0.10 + emerge -av gst-python gst-plugins-good gst-plugins-ugly gst-plugins-meta - ``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you - want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc. + ``gst-plugins-meta`` is the one that actually pulls in the plugins you want, + so pay attention to the USE flags, e.g. ``alsa``, ``mp3``, etc. #. Install the latest release of Mopidy:: From efbfb39e868d1782ee1a7ce74a127c0bca22405b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 29 Nov 2015 02:42:46 +0100 Subject: [PATCH 61/92] docs: Update changelog --- docs/changelog.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 22d80ad3..b8d0ee02 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,12 @@ v1.2.0 (UNRELEASED) Feature release. +Dependencies +------------ + +- Mopidy now requires GStreamer 1.x, as we've finally ported from GStreamer + 0.10. + Core API -------- @@ -123,6 +129,14 @@ Cleanups - Catch errors when loading :confval:`logging/config_file`. (Fixes: :issue:`1320`) +Audio +----- + +- **Breaking:** The audio scanner now returns ISO-8601 formatted strings + instead of :class:`~datetime.datetime` objects for dates found in tags. + Because of this change, we can now return years without months or days, which + matches the semantics of the date fields in our data models. + Gapless ------- From 45dae063474950aadc54b450a6a669056907ca02 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 01:11:15 +0100 Subject: [PATCH 62/92] gst1: Keep the pending URI for the stream_changed event --- mopidy/audio/actor.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 40e32992..aeace16d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -261,7 +261,7 @@ class _Handler(object): if GstPbutils.is_missing_plugin_message(msg): self.on_missing_plugin(msg) elif msg.type == Gst.MessageType.STREAM_START: - self.on_stream_changed(self._audio._playbin.get_property('uri')) + self.on_stream_start() def on_pad_event(self, pad, pad_probe_info): event = pad_probe_info.get_event() @@ -374,8 +374,9 @@ class _Handler(object): # can provide a 'mopidy install-missing-plugins' if the system has the # required helper installed? - def on_stream_changed(self, uri): - gst_logger.debug('Got STREAM_CHANGED bus message: uri=%r', uri) + def on_stream_start(self): + gst_logger.debug('Got STREAM_START bus message') + uri = self._audio._pending_uri logger.debug('Audio event: stream_changed(uri=%r)', uri) AudioListener.send('stream_changed', uri=uri) @@ -415,6 +416,7 @@ class Audio(pykka.ThreadingActor): self._target_state = Gst.State.NULL self._buffering = False self._tags = {} + self._pending_uri = None self._playbin = None self._outputs = None @@ -561,6 +563,7 @@ class Audio(pykka.ThreadingActor): current_volume = None self._tags = {} # TODO: add test for this somehow + self._pending_uri = uri self._playbin.set_property('uri', uri) if self.mixer is not None and current_volume is not None: @@ -586,7 +589,9 @@ class Audio(pykka.ThreadingActor): """ self._appsrc.prepare( Gst.Caps.from_string(caps), need_data, enough_data, seek_data) - self._playbin.set_property('uri', 'appsrc://') + uri = 'appsrc://' + self._pending_uri = uri + self._playbin.set_property('uri', uri) def emit_data(self, buffer_): """ From ef5281488b41485c01bbd588070bd81c376e9f14 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 13:51:13 +0100 Subject: [PATCH 63/92] gst1: Fix buffer.pts not being set if 0 --- mopidy/audio/utils.py | 4 ++-- tests/audio/test_utils.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index cc312b73..0f7f1957 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -29,9 +29,9 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): ``capabilites`` argument is no longer in use """ buffer_ = Gst.Buffer.new_wrapped(data) - if timestamp: + if timestamp is not None: buffer_.pts = timestamp - if duration: + if duration is not None: buffer_.duration = duration return buffer_ diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index 0b497dad..d3e81ef2 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -3,10 +3,27 @@ from __future__ import absolute_import, unicode_literals import datetime import unittest +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst + +import pytest + from mopidy.audio import utils from mopidy.models import Album, Artist, Track +class TestCreateBuffer(object): + + def test_creates_buffer(self): + buf = utils.create_buffer(b'123', timestamp=0, duration=1000000) + + assert isinstance(buf, Gst.Buffer) + assert buf.pts == 0 + assert buf.duration == 1000000 + assert buf.get_size() == len(b'123') + + # TODO: keep ids without name? # TODO: current test is trying to test everything at once with a complete tags # set, instead we might want to try with a minimal one making testing easier. From 7926ef1f127e89a9923545da665b8829087894c7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 4 Dec 2015 13:52:05 +0100 Subject: [PATCH 64/92] gst1: Fail if trying to create buffers without audio Which causes lots of failed assertion messages from GStreamer --- mopidy/audio/utils.py | 3 +++ tests/audio/test_utils.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 0f7f1957..a8627001 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -28,6 +28,9 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): .. versionchanged:: 1.2 ``capabilites`` argument is no longer in use """ + if not data: + raise ValueError( + 'Cannot create buffer without data: length=%d' % len(data)) buffer_ = Gst.Buffer.new_wrapped(data) if timestamp is not None: buffer_.pts = timestamp diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index d3e81ef2..e10613d2 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -23,6 +23,12 @@ class TestCreateBuffer(object): assert buf.duration == 1000000 assert buf.get_size() == len(b'123') + def test_fails_if_data_has_zero_length(self): + with pytest.raises(ValueError) as excinfo: + utils.create_buffer(b'', timestamp=0, duration=1000000) + + assert 'Cannot create buffer without data' in str(excinfo.value) + # TODO: keep ids without name? # TODO: current test is trying to test everything at once with a complete tags From bf6e97e5b9a45b023f24e0b6e04673cd71ddaebe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 14 Dec 2015 22:22:56 +0100 Subject: [PATCH 65/92] gst1: Fix querying of duration of MP3s --- mopidy/audio/scan.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index fb0773d6..188eb26c 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -115,13 +115,23 @@ def _start_pipeline(pipeline): pipeline.set_state(Gst.State.PLAYING) -def _query_duration(pipeline): +def _query_duration(pipeline, timeout=100): success, duration = pipeline.query_duration(Gst.Format.TIME) + if success and duration >= 0: + return duration // Gst.MSECOND - if not success or duration < 0: + result = pipeline.set_state(Gst.State.PLAYING) + if result == Gst.StateChangeReturn.FAILURE: return None - return duration // Gst.MSECOND + 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): From 844dc257df5107c77253a7158ab60924c976d2e2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 17 Dec 2015 22:02:56 +0100 Subject: [PATCH 66/92] audio: Don't bother creating decoders in audio scanner The decoders don't produce metadata and to the best of my knowledge we don't need the raw audio for duration calculation. But to play it safe this keeps in place the caps check in pad added to trigger 'have-audio'. --- mopidy/audio/scan.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 188eb26c..c99d86ef 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -13,6 +13,17 @@ from mopidy import exceptions from mopidy.audio import utils from mopidy.internal import encoding +# GST_ELEMENT_FACTORY_LIST: +_DECODER = 1 << 0 +_AUDIO = 1 << 50 +_DEMUXER = 1 << 5 +_DEPAYLOADER = 1 << 8 +_PARSER = 1 << 6 + +# GST_TYPE_AUTOPLUG_SELECT_RESULT: +_SELECT_TRY = 0 +_SELECT_EXPOSE = 1 + _Result = collections.namedtuple( 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) @@ -85,6 +96,7 @@ def _setup_pipeline(uri, proxy_config=None): typefind.connect('have-type', _have_type, decodebin) decodebin.connect('pad-added', _pad_added, pipeline) + decodebin.connect('autoplug-select', _autoplug_select) return pipeline @@ -105,10 +117,21 @@ def _pad_added(element, pad, pipeline): pad.link(sink.get_static_pad('sink')) if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')): + # Probably won't happen due to autoplug-select fix, but lets play it + # safe until we've tested more. struct = Gst.Structure.new_empty('have-audio') element.get_bus().post(Gst.Message.new_application(element, struct)) +def _autoplug_select(element, pad, caps, factory): + if factory.list_is_type(_DECODER | _AUDIO): + struct = Gst.Structure.new_empty('have-audio') + element.get_bus().post(Gst.Message.new_application(element, struct)) + if not factory.list_is_type(_DEMUXER | _DEPAYLOADER | _PARSER): + return _SELECT_EXPOSE + return _SELECT_TRY + + def _start_pipeline(pipeline): result = pipeline.set_state(Gst.State.PAUSED) if result == Gst.StateChangeReturn.NO_PREROLL: From b3aeb9b50838f325ac8df5e19eb97a10b75be7d1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 17 Dec 2015 22:10:48 +0100 Subject: [PATCH 67/92] audio: Move signal helper to utils. --- mopidy/audio/actor.py | 36 +++--------------------------------- mopidy/audio/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index aeace16d..193d825e 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -32,43 +32,13 @@ _GST_STATE_MAPPING = { } -class _Signals(object): - - """Helper for tracking gobject signal registrations""" - - def __init__(self): - self._ids = {} - - def connect(self, element, event, func, *args): - """Connect a function + args to signal event on an element. - - Each event may only be handled by one callback in this implementation. - """ - assert (element, event) not in self._ids - self._ids[(element, event)] = element.connect(event, func, *args) - - def disconnect(self, element, event): - """Disconnect whatever handler we have for and element+event pair. - - Does nothing it the handler has already been removed. - """ - signal_id = self._ids.pop((element, event), None) - if signal_id is not None: - element.disconnect(signal_id) - - def clear(self): - """Clear all registered signal handlers.""" - for element, event in self._ids.keys(): - element.disconnect(self._ids.pop((element, event))) - - # TODO: expose this as a property on audio? class _Appsrc(object): """Helper class for dealing with appsrc based playback.""" def __init__(self): - self._signals = _Signals() + self._signals = utils.Signals() self.reset() def reset(self): @@ -181,7 +151,7 @@ class SoftwareMixer(object): self._element = None self._last_volume = None self._last_mute = None - self._signals = _Signals() + self._signals = utils.Signals() def setup(self, element, mixer_ref): self._element = element @@ -424,7 +394,7 @@ class Audio(pykka.ThreadingActor): self._handler = _Handler(self) self._appsrc = _Appsrc() - self._signals = _Signals() + self._signals = utils.Signals() if mixer and self._config['audio']['mixer'] == 'software': self.mixer = SoftwareMixer(mixer) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index a8627001..6c38c058 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -193,3 +193,33 @@ gstreamer-GstTagList.html logger.debug('Ignoring unknown tag data: %r = %r', tag, value) return result + + +class Signals(object): + + """Helper for tracking gobject signal registrations""" + + def __init__(self): + self._ids = {} + + def connect(self, element, event, func, *args): + """Connect a function + args to signal event on an element. + + Each event may only be handled by one callback in this implementation. + """ + assert (element, event) not in self._ids + self._ids[(element, event)] = element.connect(event, func, *args) + + def disconnect(self, element, event): + """Disconnect whatever handler we have for and element+event pair. + + Does nothing it the handler has already been removed. + """ + signal_id = self._ids.pop((element, event), None) + if signal_id is not None: + element.disconnect(signal_id) + + def clear(self): + """Clear all registered signal handlers.""" + for element, event in self._ids.keys(): + element.disconnect(self._ids.pop((element, event))) From ded059b5c97d60e4e59b3c5e3f50279812ea968b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 17 Dec 2015 22:14:39 +0100 Subject: [PATCH 68/92] audio: Cleanup the signals we connect in the scanner Without this fix we simply crash due to using up all the available FDs on the system. --- mopidy/audio/scan.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index c99d86ef..f7d8fd67 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -61,7 +61,7 @@ class Scanner(object): """ timeout = int(timeout or self._timeout_ms) tags, duration, seekable, mime = None, None, None, None - pipeline = _setup_pipeline(uri, self._proxy_config) + pipeline, signals = _setup_pipeline(uri, self._proxy_config) try: _start_pipeline(pipeline) @@ -69,6 +69,7 @@ class Scanner(object): duration = _query_duration(pipeline) seekable = _query_seekable(pipeline) finally: + signals.clear() pipeline.set_state(Gst.State.NULL) del pipeline @@ -94,11 +95,12 @@ def _setup_pipeline(uri, proxy_config=None): if proxy_config: utils.setup_proxy(src, proxy_config) - typefind.connect('have-type', _have_type, decodebin) - decodebin.connect('pad-added', _pad_added, pipeline) - decodebin.connect('autoplug-select', _autoplug_select) + signals = utils.Signals() + signals.connect(typefind, 'have-type', _have_type, decodebin) + signals.connect(decodebin, 'pad-added', _pad_added, pipeline) + signals.connect(decodebin, 'autoplug-select', _autoplug_select) - return pipeline + return pipeline, signals def _have_type(element, probability, caps, decodebin): From f0c7d25db6b9cf844561c4351ef07ec976a7ff15 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 17 Dec 2015 22:47:26 +0100 Subject: [PATCH 69/92] audio: Reduce log level for unknown tag data --- mopidy/audio/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 6c38c058..989fac4b 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -12,6 +12,7 @@ from mopidy import compat, httpclient from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) +TRACE = logging.getLevelName('TRACE') def calculate_duration(num_samples, sample_rate): @@ -190,7 +191,8 @@ gstreamer-GstTagList.html if isinstance(value, (compat.string_types, bool, numbers.Number)): result[tag].append(value) else: - logger.debug('Ignoring unknown tag data: %r = %r', tag, value) + logger.log( + TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) return result From 31c894030d01a47255f727f79a3f0735df025f4c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 17 Dec 2015 23:57:03 +0100 Subject: [PATCH 70/92] audio: Move tag helpers to mopidy.audio.tags --- mopidy/audio/actor.py | 4 +- mopidy/audio/scan.py | 4 +- mopidy/audio/tags.py | 132 +++++++++++++++++++ mopidy/audio/utils.py | 123 +----------------- mopidy/file/library.py | 4 +- mopidy/local/commands.py | 12 +- mopidy/stream/actor.py | 4 +- tests/audio/test_tags.py | 261 ++++++++++++++++++++++++++++++++++++++ tests/audio/test_utils.py | 258 ------------------------------------- 9 files changed, 408 insertions(+), 394 deletions(-) create mode 100644 mopidy/audio/tags.py create mode 100644 tests/audio/test_tags.py diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 193d825e..ca25f4dd 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -12,7 +12,7 @@ Gst.is_initialized() or Gst.init() import pykka from mopidy import exceptions -from mopidy.audio import utils +from mopidy.audio import tags as tags_lib, utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process @@ -325,7 +325,7 @@ class _Handler(object): gst_logger.debug('Got ASYNC_DONE bus message.') def on_tag(self, taglist): - tags = utils.convert_taglist(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()) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index f7d8fd67..ed1c6424 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -10,7 +10,7 @@ from gi.repository import Gst, GstPbutils Gst.is_initialized() or Gst.init() from mopidy import exceptions -from mopidy.audio import utils +from mopidy.audio import tags as tags_lib, utils from mopidy.internal import encoding # GST_ELEMENT_FACTORY_LIST: @@ -214,7 +214,7 @@ def _process(pipeline, timeout_ms): elif message.type == Gst.MessageType.TAG: taglist = message.parse_tag() # Note that this will only keep the last tag. - tags.update(utils.convert_taglist(taglist)) + tags.update(tags_lib.convert_taglist(taglist)) now = int(time.time() * 1000) timeout -= now - previous diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py new file mode 100644 index 00000000..ba2b021a --- /dev/null +++ b/mopidy/audio/tags.py @@ -0,0 +1,132 @@ +from __future__ import absolute_import, unicode_literals + +import collections +import logging +import numbers + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import Gst +Gst.is_initialized() or Gst.init() + +from mopidy import compat +from mopidy.models import Album, Artist, Track + + +logger = logging.getLogger(__name__) +TRACE = logging.getLevelName('TRACE') + + +def convert_taglist(taglist): + """Convert a :class:`Gst.TagList` to plain Python types. + + Knows how to convert: + + - Dates + - Buffers + - Numbers + - Strings + - Booleans + + Unknown types will be ignored and trace logged. Tag keys are all strings + defined as part GStreamer under GstTagList_. + + .. _GstTagList: https://developer.gnome.org/gstreamer/stable/\ +gstreamer-GstTagList.html + + :param taglist: A GStreamer taglist to be converted. + :type taglist: :class:`Gst.TagList` + :rtype: dictionary of tag keys with a list of values. + """ + result = collections.defaultdict(list) + + for n in range(taglist.n_tags()): + tag = taglist.nth_tag_name(n) + + for i in range(taglist.get_tag_size(tag)): + value = taglist.get_value_index(tag, i) + + if isinstance(value, Gst.DateTime): + result[tag].append(value.to_iso8601_string()) + if isinstance(value, (compat.string_types, bool, numbers.Number)): + result[tag].append(value) + else: + logger.log( + TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) + + return result + + +# 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): + """Convert our normalized tags to a track. + + :param tags: dictionary of tag keys with a list of values + :type tags: :class:`dict` + :rtype: :class:`mopidy.models.Track` + """ + album_kwargs = {} + track_kwargs = {} + + track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER) + track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER) + track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST, + 'musicbrainz-artistid', + 'musicbrainz-sortname') + album_kwargs['artists'] = _artists( + tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') + + track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, [])) + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, [])) + if not track_kwargs['name']: + track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, [])) + + track_kwargs['comment'] = '; '.join(tags.get('comment', [])) + if not track_kwargs['comment']: + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, [])) + if not track_kwargs['comment']: + track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, [])) + + track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0] + track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] + track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0] + track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0] + + album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0] + album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0] + album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] + album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] + + if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: + track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() + + # Clear out any empty values we found + track_kwargs = {k: v for k, v in track_kwargs.items() if v} + album_kwargs = {k: v for k, v in album_kwargs.items() if v} + + # Only bother with album if we have a name to show. + if album_kwargs.get('name'): + track_kwargs['album'] = Album(**album_kwargs) + + return Track(**track_kwargs) + + +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): + attrs = {'name': tags[artist_name][0]} + if artist_id in tags: + attrs['musicbrainz_id'] = tags[artist_id][0] + if artist_sortname in tags: + attrs['sortname'] = tags[artist_sortname][0] + return [Artist(**attrs)] + + # Multiple artist, provide artists with name only to avoid ambiguity. + return [Artist(name=name) for name in tags[artist_name]] diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 989fac4b..6a11c7a3 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -1,18 +1,10 @@ from __future__ import absolute_import, unicode_literals -import collections -import logging -import numbers - import gi gi.require_version('Gst', '1.0') from gi.repository import Gst -from mopidy import compat, httpclient -from mopidy.models import Album, Artist, Track - -logger = logging.getLogger(__name__) -TRACE = logging.getLevelName('TRACE') +from mopidy import httpclient def calculate_duration(num_samples, sample_rate): @@ -68,79 +60,6 @@ def supported_uri_schemes(uri_schemes): return supported_schemes -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): - attrs = {'name': tags[artist_name][0]} - if artist_id in tags: - attrs['musicbrainz_id'] = tags[artist_id][0] - if artist_sortname in tags: - attrs['sortname'] = tags[artist_sortname][0] - return [Artist(**attrs)] - - # Multiple artist, provide artists with name only to avoid ambiguity. - return [Artist(name=name) for name in tags[artist_name]] - - -# 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): - """Convert our normalized tags to a track. - - :param tags: dictionary of tag keys with a list of values - :type tags: :class:`dict` - :rtype: :class:`mopidy.models.Track` - """ - album_kwargs = {} - track_kwargs = {} - - track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER) - track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER) - track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST, - 'musicbrainz-artistid', - 'musicbrainz-sortname') - album_kwargs['artists'] = _artists( - tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid') - - track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, [])) - track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, [])) - if not track_kwargs['name']: - track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, [])) - - track_kwargs['comment'] = '; '.join(tags.get('comment', [])) - if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, [])) - if not track_kwargs['comment']: - track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, [])) - - track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0] - track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0] - track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0] - track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0] - - album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0] - album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0] - album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] - album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - - if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: - track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() - - # Clear out any empty values we found - track_kwargs = {k: v for k, v in track_kwargs.items() if v} - album_kwargs = {k: v for k, v in album_kwargs.items() if v} - - # Only bother with album if we have a name to show. - if album_kwargs.get('name'): - track_kwargs['album'] = Album(**album_kwargs) - - return Track(**track_kwargs) - - def setup_proxy(element, config): """Configure a GStreamer element with proxy settings. @@ -157,46 +76,6 @@ def setup_proxy(element, config): element.set_property('proxy-pw', config.get('password')) -def convert_taglist(taglist): - """Convert a :class:`Gst.TagList` to plain Python types. - - Knows how to convert: - - - Dates - - Buffers - - Numbers - - Strings - - Booleans - - Unknown types will be ignored and debug logged. Tag keys are all strings - defined as part GStreamer under GstTagList_. - - .. _GstTagList: https://developer.gnome.org/gstreamer/stable/\ -gstreamer-GstTagList.html - - :param taglist: A GStreamer taglist to be converted. - :type taglist: :class:`Gst.TagList` - :rtype: dictionary of tag keys with a list of values. - """ - result = collections.defaultdict(list) - - for n in range(taglist.n_tags()): - tag = taglist.nth_tag_name(n) - - for i in range(taglist.get_tag_size(tag)): - value = taglist.get_value_index(tag, i) - - if isinstance(value, Gst.DateTime): - result[tag].append(value.to_iso8601_string()) - if isinstance(value, (compat.string_types, bool, numbers.Number)): - result[tag].append(value) - else: - logger.log( - TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) - - return result - - class Signals(object): """Helper for tracking gobject signal registrations""" diff --git a/mopidy/file/library.py b/mopidy/file/library.py index 20ac0632..09fa2cf1 100644 --- a/mopidy/file/library.py +++ b/mopidy/file/library.py @@ -7,7 +7,7 @@ import sys import urllib2 from mopidy import backend, exceptions, models -from mopidy.audio import scan, utils +from mopidy.audio import scan, tags from mopidy.internal import path @@ -83,7 +83,7 @@ class FileLibraryProvider(backend.LibraryProvider): try: result = self._scanner.scan(uri) - track = utils.convert_tags_to_track(result.tags).copy( + track = tags.convert_tags_to_track(result.tags).copy( uri=uri, length=result.duration) except exceptions.ScannerError as e: logger.warning('Failed looking up %s: %s', uri, e) diff --git a/mopidy/local/commands.py b/mopidy/local/commands.py index d61cf441..ead874a0 100644 --- a/mopidy/local/commands.py +++ b/mopidy/local/commands.py @@ -6,7 +6,7 @@ import os import time from mopidy import commands, compat, exceptions -from mopidy.audio import scan, utils +from mopidy.audio import scan, tags from mopidy.internal import path from mopidy.local import translator @@ -140,18 +140,18 @@ class ScanCommand(commands.Command): relpath = translator.local_track_uri_to_path(uri, media_dir) file_uri = path.path_to_uri(os.path.join(media_dir, relpath)) result = scanner.scan(file_uri) - tags, duration = result.tags, result.duration if not result.playable: logger.warning('Failed %s: No audio found in file.', uri) - elif duration < MIN_DURATION_MS: + elif result.duration < MIN_DURATION_MS: logger.warning('Failed %s: Track shorter than %dms', uri, MIN_DURATION_MS) else: mtime = file_mtimes.get(os.path.join(media_dir, relpath)) - track = utils.convert_tags_to_track(tags).replace( - uri=uri, length=duration, last_modified=mtime) + track = tags.convert_tags_to_track(result.tags).replace( + uri=uri, length=result.duration, last_modified=mtime) if library.add_supports_tags_and_duration: - library.add(track, tags=tags, duration=duration) + library.add( + track, tags=result.tags, duration=result.duration) else: library.add(track) logger.debug('Added %s', track.uri) diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py index 5f88b13b..c2e39652 100644 --- a/mopidy/stream/actor.py +++ b/mopidy/stream/actor.py @@ -8,7 +8,7 @@ import time import pykka from mopidy import audio as audio_lib, backend, exceptions, stream -from mopidy.audio import scan, utils +from mopidy.audio import scan, tags from mopidy.compat import urllib from mopidy.internal import http, playlists from mopidy.models import Track @@ -60,7 +60,7 @@ class StreamLibraryProvider(backend.LibraryProvider): try: result = self._scanner.scan(uri) - track = utils.convert_tags_to_track(result.tags).replace( + 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) diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py new file mode 100644 index 00000000..355af68e --- /dev/null +++ b/tests/audio/test_tags.py @@ -0,0 +1,261 @@ +from __future__ import absolute_import, unicode_literals + +import datetime +import unittest + +from mopidy.audio import tags +from mopidy.models import Album, Artist, Track + + +# TODO: keep ids without name? +# TODO: current test is trying to test everything at once with a complete tags +# set, instead we might want to try with a minimal one making testing easier. +class TagsToTrackTest(unittest.TestCase): + + def setUp(self): # noqa: N802 + self.tags = { + 'album': ['album'], + 'track-number': [1], + 'artist': ['artist'], + 'composer': ['composer'], + 'performer': ['performer'], + 'album-artist': ['albumartist'], + 'title': ['track'], + 'track-count': [2], + 'album-disc-number': [2], + 'album-disc-count': [3], + 'date': [datetime.date(2006, 1, 1,)], + 'container-format': ['ID3 tag'], + 'genre': ['genre'], + 'comment': ['comment'], + 'musicbrainz-trackid': ['trackid'], + 'musicbrainz-albumid': ['albumid'], + 'musicbrainz-artistid': ['artistid'], + 'musicbrainz-sortname': ['sortname'], + 'musicbrainz-albumartistid': ['albumartistid'], + 'bitrate': [1000], + } + + artist = Artist(name='artist', musicbrainz_id='artistid', + sortname='sortname') + composer = Artist(name='composer') + performer = Artist(name='performer') + albumartist = Artist(name='albumartist', + musicbrainz_id='albumartistid') + + album = Album(name='album', num_tracks=2, num_discs=3, + musicbrainz_id='albumid', artists=[albumartist]) + + self.track = Track(name='track', date='2006-01-01', + genre='genre', track_no=1, disc_no=2, + comment='comment', musicbrainz_id='trackid', + album=album, bitrate=1000, artists=[artist], + composers=[composer], performers=[performer]) + + def check(self, expected): + actual = tags.convert_tags_to_track(self.tags) + self.assertEqual(expected, actual) + + def test_track(self): + self.check(self.track) + + def test_missing_track_no(self): + del self.tags['track-number'] + self.check(self.track.replace(track_no=None)) + + def test_multiple_track_no(self): + self.tags['track-number'].append(9) + self.check(self.track) + + def test_missing_track_disc_no(self): + del self.tags['album-disc-number'] + self.check(self.track.replace(disc_no=None)) + + def test_multiple_track_disc_no(self): + self.tags['album-disc-number'].append(9) + self.check(self.track) + + def test_missing_track_name(self): + del self.tags['title'] + self.check(self.track.replace(name=None)) + + def test_multiple_track_name(self): + self.tags['title'] = ['name1', 'name2'] + self.check(self.track.replace(name='name1; name2')) + + def test_missing_track_musicbrainz_id(self): + del self.tags['musicbrainz-trackid'] + self.check(self.track.replace(musicbrainz_id=None)) + + def test_multiple_track_musicbrainz_id(self): + self.tags['musicbrainz-trackid'].append('id') + self.check(self.track) + + def test_missing_track_bitrate(self): + del self.tags['bitrate'] + self.check(self.track.replace(bitrate=None)) + + def test_multiple_track_bitrate(self): + self.tags['bitrate'].append(1234) + self.check(self.track) + + def test_missing_track_genre(self): + del self.tags['genre'] + self.check(self.track.replace(genre=None)) + + def test_multiple_track_genre(self): + self.tags['genre'] = ['genre1', 'genre2'] + self.check(self.track.replace(genre='genre1; genre2')) + + def test_missing_track_date(self): + del self.tags['date'] + self.check(self.track.replace(date=None)) + + def test_multiple_track_date(self): + self.tags['date'].append(datetime.date(2030, 1, 1)) + self.check(self.track) + + def test_missing_track_comment(self): + del self.tags['comment'] + self.check(self.track.replace(comment=None)) + + def test_multiple_track_comment(self): + self.tags['comment'] = ['comment1', 'comment2'] + self.check(self.track.replace(comment='comment1; comment2')) + + def test_missing_track_artist_name(self): + del self.tags['artist'] + self.check(self.track.replace(artists=[])) + + def test_multiple_track_artist_name(self): + self.tags['artist'] = ['name1', 'name2'] + artists = [Artist(name='name1'), Artist(name='name2')] + self.check(self.track.replace(artists=artists)) + + def test_missing_track_artist_musicbrainz_id(self): + del self.tags['musicbrainz-artistid'] + artist = list(self.track.artists)[0].replace(musicbrainz_id=None) + self.check(self.track.replace(artists=[artist])) + + def test_multiple_track_artist_musicbrainz_id(self): + self.tags['musicbrainz-artistid'].append('id') + self.check(self.track) + + def test_missing_track_composer_name(self): + del self.tags['composer'] + self.check(self.track.replace(composers=[])) + + def test_multiple_track_composer_name(self): + self.tags['composer'] = ['composer1', 'composer2'] + composers = [Artist(name='composer1'), Artist(name='composer2')] + self.check(self.track.replace(composers=composers)) + + def test_missing_track_performer_name(self): + del self.tags['performer'] + self.check(self.track.replace(performers=[])) + + def test_multiple_track_performe_name(self): + self.tags['performer'] = ['performer1', 'performer2'] + performers = [Artist(name='performer1'), Artist(name='performer2')] + self.check(self.track.replace(performers=performers)) + + def test_missing_album_name(self): + del self.tags['album'] + self.check(self.track.replace(album=None)) + + def test_multiple_album_name(self): + self.tags['album'].append('album2') + self.check(self.track) + + def test_missing_album_musicbrainz_id(self): + del self.tags['musicbrainz-albumid'] + album = self.track.album.replace(musicbrainz_id=None, + images=[]) + self.check(self.track.replace(album=album)) + + def test_multiple_album_musicbrainz_id(self): + self.tags['musicbrainz-albumid'].append('id') + self.check(self.track) + + def test_missing_album_num_tracks(self): + del self.tags['track-count'] + album = self.track.album.replace(num_tracks=None) + self.check(self.track.replace(album=album)) + + def test_multiple_album_num_tracks(self): + self.tags['track-count'].append(9) + self.check(self.track) + + def test_missing_album_num_discs(self): + del self.tags['album-disc-count'] + album = self.track.album.replace(num_discs=None) + self.check(self.track.replace(album=album)) + + def test_multiple_album_num_discs(self): + self.tags['album-disc-count'].append(9) + self.check(self.track) + + def test_missing_album_artist_name(self): + del self.tags['album-artist'] + album = self.track.album.replace(artists=[]) + self.check(self.track.replace(album=album)) + + def test_multiple_album_artist_name(self): + self.tags['album-artist'] = ['name1', 'name2'] + artists = [Artist(name='name1'), Artist(name='name2')] + album = self.track.album.replace(artists=artists) + self.check(self.track.replace(album=album)) + + def test_missing_album_artist_musicbrainz_id(self): + del self.tags['musicbrainz-albumartistid'] + albumartist = list(self.track.album.artists)[0] + albumartist = albumartist.replace(musicbrainz_id=None) + album = self.track.album.replace(artists=[albumartist]) + self.check(self.track.replace(album=album)) + + def test_multiple_album_artist_musicbrainz_id(self): + self.tags['musicbrainz-albumartistid'].append('id') + self.check(self.track) + + def test_stream_organization_track_name(self): + del self.tags['title'] + self.tags['organization'] = ['organization'] + self.check(self.track.replace(name='organization')) + + def test_multiple_organization_track_name(self): + del self.tags['title'] + self.tags['organization'] = ['organization1', 'organization2'] + self.check(self.track.replace(name='organization1; organization2')) + + # TODO: combine all comment types? + def test_stream_location_track_comment(self): + del self.tags['comment'] + self.tags['location'] = ['location'] + self.check(self.track.replace(comment='location')) + + def test_multiple_location_track_comment(self): + del self.tags['comment'] + self.tags['location'] = ['location1', 'location2'] + self.check(self.track.replace(comment='location1; location2')) + + def test_stream_copyright_track_comment(self): + del self.tags['comment'] + self.tags['copyright'] = ['copyright'] + self.check(self.track.replace(comment='copyright')) + + def test_multiple_copyright_track_comment(self): + del self.tags['comment'] + self.tags['copyright'] = ['copyright1', 'copyright2'] + self.check(self.track.replace(comment='copyright1; copyright2')) + + def test_sortname(self): + self.tags['musicbrainz-sortname'] = ['another_sortname'] + artist = Artist(name='artist', sortname='another_sortname', + musicbrainz_id='artistid') + self.check(self.track.replace(artists=[artist])) + + def test_missing_sortname(self): + del self.tags['musicbrainz-sortname'] + artist = Artist(name='artist', sortname=None, + musicbrainz_id='artistid') + self.check(self.track.replace(artists=[artist])) diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index e10613d2..0ce15bcb 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -1,8 +1,5 @@ from __future__ import absolute_import, unicode_literals -import datetime -import unittest - import gi gi.require_version('Gst', '1.0') from gi.repository import Gst @@ -10,7 +7,6 @@ from gi.repository import Gst import pytest from mopidy.audio import utils -from mopidy.models import Album, Artist, Track class TestCreateBuffer(object): @@ -28,257 +24,3 @@ class TestCreateBuffer(object): utils.create_buffer(b'', timestamp=0, duration=1000000) assert 'Cannot create buffer without data' in str(excinfo.value) - - -# TODO: keep ids without name? -# TODO: current test is trying to test everything at once with a complete tags -# set, instead we might want to try with a minimal one making testing easier. -class TagsToTrackTest(unittest.TestCase): - - def setUp(self): # noqa: N802 - self.tags = { - 'album': ['album'], - 'track-number': [1], - 'artist': ['artist'], - 'composer': ['composer'], - 'performer': ['performer'], - 'album-artist': ['albumartist'], - 'title': ['track'], - 'track-count': [2], - 'album-disc-number': [2], - 'album-disc-count': [3], - 'date': [datetime.date(2006, 1, 1,)], - 'container-format': ['ID3 tag'], - 'genre': ['genre'], - 'comment': ['comment'], - 'musicbrainz-trackid': ['trackid'], - 'musicbrainz-albumid': ['albumid'], - 'musicbrainz-artistid': ['artistid'], - 'musicbrainz-sortname': ['sortname'], - 'musicbrainz-albumartistid': ['albumartistid'], - 'bitrate': [1000], - } - - artist = Artist(name='artist', musicbrainz_id='artistid', - sortname='sortname') - composer = Artist(name='composer') - performer = Artist(name='performer') - albumartist = Artist(name='albumartist', - musicbrainz_id='albumartistid') - - album = Album(name='album', num_tracks=2, num_discs=3, - musicbrainz_id='albumid', artists=[albumartist]) - - self.track = Track(name='track', date='2006-01-01', - genre='genre', track_no=1, disc_no=2, - comment='comment', musicbrainz_id='trackid', - album=album, bitrate=1000, artists=[artist], - composers=[composer], performers=[performer]) - - def check(self, expected): - actual = utils.convert_tags_to_track(self.tags) - self.assertEqual(expected, actual) - - def test_track(self): - self.check(self.track) - - def test_missing_track_no(self): - del self.tags['track-number'] - self.check(self.track.replace(track_no=None)) - - def test_multiple_track_no(self): - self.tags['track-number'].append(9) - self.check(self.track) - - def test_missing_track_disc_no(self): - del self.tags['album-disc-number'] - self.check(self.track.replace(disc_no=None)) - - def test_multiple_track_disc_no(self): - self.tags['album-disc-number'].append(9) - self.check(self.track) - - def test_missing_track_name(self): - del self.tags['title'] - self.check(self.track.replace(name=None)) - - def test_multiple_track_name(self): - self.tags['title'] = ['name1', 'name2'] - self.check(self.track.replace(name='name1; name2')) - - def test_missing_track_musicbrainz_id(self): - del self.tags['musicbrainz-trackid'] - self.check(self.track.replace(musicbrainz_id=None)) - - def test_multiple_track_musicbrainz_id(self): - self.tags['musicbrainz-trackid'].append('id') - self.check(self.track) - - def test_missing_track_bitrate(self): - del self.tags['bitrate'] - self.check(self.track.replace(bitrate=None)) - - def test_multiple_track_bitrate(self): - self.tags['bitrate'].append(1234) - self.check(self.track) - - def test_missing_track_genre(self): - del self.tags['genre'] - self.check(self.track.replace(genre=None)) - - def test_multiple_track_genre(self): - self.tags['genre'] = ['genre1', 'genre2'] - self.check(self.track.replace(genre='genre1; genre2')) - - def test_missing_track_date(self): - del self.tags['date'] - self.check(self.track.replace(date=None)) - - def test_multiple_track_date(self): - self.tags['date'].append(datetime.date(2030, 1, 1)) - self.check(self.track) - - def test_missing_track_comment(self): - del self.tags['comment'] - self.check(self.track.replace(comment=None)) - - def test_multiple_track_comment(self): - self.tags['comment'] = ['comment1', 'comment2'] - self.check(self.track.replace(comment='comment1; comment2')) - - def test_missing_track_artist_name(self): - del self.tags['artist'] - self.check(self.track.replace(artists=[])) - - def test_multiple_track_artist_name(self): - self.tags['artist'] = ['name1', 'name2'] - artists = [Artist(name='name1'), Artist(name='name2')] - self.check(self.track.replace(artists=artists)) - - def test_missing_track_artist_musicbrainz_id(self): - del self.tags['musicbrainz-artistid'] - artist = list(self.track.artists)[0].replace(musicbrainz_id=None) - self.check(self.track.replace(artists=[artist])) - - def test_multiple_track_artist_musicbrainz_id(self): - self.tags['musicbrainz-artistid'].append('id') - self.check(self.track) - - def test_missing_track_composer_name(self): - del self.tags['composer'] - self.check(self.track.replace(composers=[])) - - def test_multiple_track_composer_name(self): - self.tags['composer'] = ['composer1', 'composer2'] - composers = [Artist(name='composer1'), Artist(name='composer2')] - self.check(self.track.replace(composers=composers)) - - def test_missing_track_performer_name(self): - del self.tags['performer'] - self.check(self.track.replace(performers=[])) - - def test_multiple_track_performe_name(self): - self.tags['performer'] = ['performer1', 'performer2'] - performers = [Artist(name='performer1'), Artist(name='performer2')] - self.check(self.track.replace(performers=performers)) - - def test_missing_album_name(self): - del self.tags['album'] - self.check(self.track.replace(album=None)) - - def test_multiple_album_name(self): - self.tags['album'].append('album2') - self.check(self.track) - - def test_missing_album_musicbrainz_id(self): - del self.tags['musicbrainz-albumid'] - album = self.track.album.replace(musicbrainz_id=None, - images=[]) - self.check(self.track.replace(album=album)) - - def test_multiple_album_musicbrainz_id(self): - self.tags['musicbrainz-albumid'].append('id') - self.check(self.track) - - def test_missing_album_num_tracks(self): - del self.tags['track-count'] - album = self.track.album.replace(num_tracks=None) - self.check(self.track.replace(album=album)) - - def test_multiple_album_num_tracks(self): - self.tags['track-count'].append(9) - self.check(self.track) - - def test_missing_album_num_discs(self): - del self.tags['album-disc-count'] - album = self.track.album.replace(num_discs=None) - self.check(self.track.replace(album=album)) - - def test_multiple_album_num_discs(self): - self.tags['album-disc-count'].append(9) - self.check(self.track) - - def test_missing_album_artist_name(self): - del self.tags['album-artist'] - album = self.track.album.replace(artists=[]) - self.check(self.track.replace(album=album)) - - def test_multiple_album_artist_name(self): - self.tags['album-artist'] = ['name1', 'name2'] - artists = [Artist(name='name1'), Artist(name='name2')] - album = self.track.album.replace(artists=artists) - self.check(self.track.replace(album=album)) - - def test_missing_album_artist_musicbrainz_id(self): - del self.tags['musicbrainz-albumartistid'] - albumartist = list(self.track.album.artists)[0] - albumartist = albumartist.replace(musicbrainz_id=None) - album = self.track.album.replace(artists=[albumartist]) - self.check(self.track.replace(album=album)) - - def test_multiple_album_artist_musicbrainz_id(self): - self.tags['musicbrainz-albumartistid'].append('id') - self.check(self.track) - - def test_stream_organization_track_name(self): - del self.tags['title'] - self.tags['organization'] = ['organization'] - self.check(self.track.replace(name='organization')) - - def test_multiple_organization_track_name(self): - del self.tags['title'] - self.tags['organization'] = ['organization1', 'organization2'] - self.check(self.track.replace(name='organization1; organization2')) - - # TODO: combine all comment types? - def test_stream_location_track_comment(self): - del self.tags['comment'] - self.tags['location'] = ['location'] - self.check(self.track.replace(comment='location')) - - def test_multiple_location_track_comment(self): - del self.tags['comment'] - self.tags['location'] = ['location1', 'location2'] - self.check(self.track.replace(comment='location1; location2')) - - def test_stream_copyright_track_comment(self): - del self.tags['comment'] - self.tags['copyright'] = ['copyright'] - self.check(self.track.replace(comment='copyright')) - - def test_multiple_copyright_track_comment(self): - del self.tags['comment'] - self.tags['copyright'] = ['copyright1', 'copyright2'] - self.check(self.track.replace(comment='copyright1; copyright2')) - - def test_sortname(self): - self.tags['musicbrainz-sortname'] = ['another_sortname'] - artist = Artist(name='artist', sortname='another_sortname', - musicbrainz_id='artistid') - self.check(self.track.replace(artists=[artist])) - - def test_missing_sortname(self): - del self.tags['musicbrainz-sortname'] - artist = Artist(name='artist', sortname=None, - musicbrainz_id='artistid') - self.check(self.track.replace(artists=[artist])) From 9fde0bec553fe8535edad36a3bae3970d167eb9b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 01:54:55 +0100 Subject: [PATCH 71/92] audio, timer: Fix trace log stmt --- mopidy/audio/tags.py | 5 +++-- mopidy/internal/timer.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index ba2b021a..746c1aff 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -10,11 +10,11 @@ from gi.repository import Gst Gst.is_initialized() or Gst.init() from mopidy import compat +from mopidy.internal import log from mopidy.models import Album, Artist, Track logger = logging.getLogger(__name__) -TRACE = logging.getLevelName('TRACE') def convert_taglist(taglist): @@ -52,7 +52,8 @@ gstreamer-GstTagList.html result[tag].append(value) else: logger.log( - TRACE, 'Ignoring unknown tag data: %r = %r', tag, value) + log.TRACE_LOG_LEVEL, + 'Ignoring unknown tag data: %r = %r', tag, value) return result diff --git a/mopidy/internal/timer.py b/mopidy/internal/timer.py index b8dcb30d..7da02e55 100644 --- a/mopidy/internal/timer.py +++ b/mopidy/internal/timer.py @@ -4,13 +4,14 @@ import contextlib import logging import time +from mopidy.internal import log + logger = logging.getLogger(__name__) -TRACE = logging.getLevelName('TRACE') @contextlib.contextmanager -def time_logger(name, level=TRACE): +def time_logger(name, level=log.TRACE_LOG_LEVEL): start = time.time() yield logger.log(level, '%s took %dms', name, (time.time() - start) * 1000) From 8b543bad44ab62877e5074f409da9c6c97fa7a20 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 00:05:25 +0100 Subject: [PATCH 72/92] local: URIs should be unicode Any non-ASCII content is uriencoded anyway. --- mopidy/local/translator.py | 4 ++-- tests/local/test_translator.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/local/translator.py b/mopidy/local/translator.py index 6fc53f63..16842f59 100644 --- a/mopidy/local/translator.py +++ b/mopidy/local/translator.py @@ -42,11 +42,11 @@ def path_to_local_track_uri(relpath): URI.""" if isinstance(relpath, compat.text_type): relpath = relpath.encode('utf-8') - return b'local:track:%s' % urllib.quote(relpath) + return 'local:track:%s' % urllib.quote(relpath) def path_to_local_directory_uri(relpath): """Convert path relative to :confval:`local/media_dir` directory URI.""" if isinstance(relpath, compat.text_type): relpath = relpath.encode('utf-8') - return b'local:directory:%s' % urllib.quote(relpath) + return 'local:directory:%s' % urllib.quote(relpath) diff --git a/tests/local/test_translator.py b/tests/local/test_translator.py index e28de173..7839cd58 100644 --- a/tests/local/test_translator.py +++ b/tests/local/test_translator.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import pytest +from mopidy import compat from mopidy.local import translator @@ -89,7 +90,9 @@ def test_path_to_file_uri(path, uri): (b'\x00\x01\x02', 'local:track:%00%01%02'), ]) def test_path_to_local_track_uri(path, uri): - assert translator.path_to_local_track_uri(path) == uri + result = translator.path_to_local_track_uri(path) + assert isinstance(result, compat.text_type) + assert result == uri @pytest.mark.parametrize('path,uri', [ @@ -99,4 +102,6 @@ def test_path_to_local_track_uri(path, uri): (b'\x00\x01\x02', 'local:directory:%00%01%02'), ]) def test_path_to_local_directory_uri(path, uri): - assert translator.path_to_local_directory_uri(path) == uri + result = translator.path_to_local_directory_uri(path) + assert isinstance(result, compat.text_type) + assert result == uri From df62997186b195abb0fc1b5344f58e82772a39fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 01:31:24 +0100 Subject: [PATCH 73/92] audio: Decode tags to unicode --- mopidy/audio/tags.py | 6 ++-- tests/audio/test_tags.py | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 746c1aff..c5376906 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -47,8 +47,10 @@ gstreamer-GstTagList.html value = taglist.get_value_index(tag, i) if isinstance(value, Gst.DateTime): - result[tag].append(value.to_iso8601_string()) - if isinstance(value, (compat.string_types, bool, numbers.Number)): + result[tag].append(value.to_iso8601_string().decode('utf-8')) + elif isinstance(value, bytes): + result[tag].append(value.decode('utf-8', 'replace')) + elif isinstance(value, (compat.text_type, bool, numbers.Number)): result[tag].append(value) else: logger.log( diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 355af68e..19a2a804 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -1,12 +1,72 @@ +# encoding: utf-8 + from __future__ import absolute_import, unicode_literals import datetime import unittest +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +from mopidy import compat from mopidy.audio import tags from mopidy.models import Album, Artist, Track +class TestConvertTaglist(object): + + def make_taglist(self, tag, values): + taglist = Gst.TagList.new_empty() + + for value in values: + if isinstance(value, Gst.DateTime): + taglist.add_value(Gst.TagMergeMode.APPEND, tag, value) + continue + + gobject_value = GObject.Value() + if isinstance(value, bytes): + gobject_value.init(GObject.TYPE_STRING) + gobject_value.set_string(value) + elif isinstance(value, int): + gobject_value.init(GObject.TYPE_UINT) + gobject_value.set_uint(value) + gobject_value.init(GObject.TYPE_VALUE) + gobject_value.set_value(value) + else: + raise TypeError + taglist.add_value(Gst.TagMergeMode.APPEND, tag, gobject_value) + + return taglist + + def test_date_time_tag(self): + taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ + Gst.DateTime.new_from_iso8601_string(b'2014-01-07') + ]) + + result = tags.convert_taglist(taglist) + + assert isinstance(result[Gst.TAG_DATE_TIME][0], compat.text_type) + assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07' + + def test_string_tag(self): + taglist = self.make_taglist(Gst.TAG_ARTIST, [b'ABBA', b'ACDC']) + + result = tags.convert_taglist(taglist) + + assert isinstance(result[Gst.TAG_ARTIST][0], compat.text_type) + assert result[Gst.TAG_ARTIST][0] == 'ABBA' + assert isinstance(result[Gst.TAG_ARTIST][1], compat.text_type) + assert result[Gst.TAG_ARTIST][1] == 'ACDC' + + def test_integer_tag(self): + taglist = self.make_taglist(Gst.TAG_BITRATE, [17]) + + result = tags.convert_taglist(taglist) + + assert result[Gst.TAG_BITRATE][0] == 17 + + # TODO: keep ids without name? # TODO: current test is trying to test everything at once with a complete tags # set, instead we might want to try with a minimal one making testing easier. From 0fa78b8e3943d871b517ba7e5cbc514b67230839 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 01:37:04 +0100 Subject: [PATCH 74/92] gst1: Fix datetime tag conversion --- mopidy/audio/tags.py | 3 +-- tests/audio/test_tags.py | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index c5376906..85b56d4f 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -101,8 +101,7 @@ def convert_tags_to_track(tags): album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - if tags.get(Gst.TAG_DATE) and tags.get(Gst.TAG_DATE)[0]: - track_kwargs['date'] = tags[Gst.TAG_DATE][0].isoformat() + track_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 19a2a804..8a1116be 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -41,13 +41,13 @@ class TestConvertTaglist(object): def test_date_time_tag(self): taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ - Gst.DateTime.new_from_iso8601_string(b'2014-01-07') + Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12') ]) result = tags.convert_taglist(taglist) assert isinstance(result[Gst.TAG_DATE_TIME][0], compat.text_type) - assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07' + assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07T14:13:12Z' def test_string_tag(self): taglist = self.make_taglist(Gst.TAG_ARTIST, [b'ABBA', b'ACDC']) @@ -84,7 +84,7 @@ class TagsToTrackTest(unittest.TestCase): 'track-count': [2], 'album-disc-number': [2], 'album-disc-count': [3], - 'date': [datetime.date(2006, 1, 1,)], + 'date': ['2006-01-01'], 'container-format': ['ID3 tag'], 'genre': ['genre'], 'comment': ['comment'], @@ -172,7 +172,7 @@ class TagsToTrackTest(unittest.TestCase): self.check(self.track.replace(date=None)) def test_multiple_track_date(self): - self.tags['date'].append(datetime.date(2030, 1, 1)) + self.tags['date'].append('2030-01-01') self.check(self.track) def test_missing_track_comment(self): From f877ac08071f1eabbeaa99856e6d442aa6567285 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 02:21:37 +0100 Subject: [PATCH 75/92] audio: Add support for GLib.Date tag values --- mopidy/audio/tags.py | 7 ++++++- tests/audio/test_tags.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 85b56d4f..78c09775 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -1,12 +1,13 @@ from __future__ import absolute_import, unicode_literals import collections +import datetime import logging import numbers import gi gi.require_version('Gst', '1.0') -from gi.repository import Gst +from gi.repository import GLib, Gst Gst.is_initialized() or Gst.init() from mopidy import compat @@ -46,6 +47,10 @@ gstreamer-GstTagList.html for i in range(taglist.get_tag_size(tag)): 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): result[tag].append(value.to_iso8601_string().decode('utf-8')) elif isinstance(value, bytes): diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 8a1116be..4619273b 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -2,12 +2,11 @@ from __future__ import absolute_import, unicode_literals -import datetime import unittest import gi gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst +from gi.repository import GLib, GObject, Gst from mopidy import compat from mopidy.audio import tags @@ -20,7 +19,7 @@ class TestConvertTaglist(object): taglist = Gst.TagList.new_empty() for value in values: - if isinstance(value, Gst.DateTime): + if isinstance(value, (GLib.Date, Gst.DateTime)): taglist.add_value(Gst.TagMergeMode.APPEND, tag, value) continue @@ -39,6 +38,15 @@ class TestConvertTaglist(object): return taglist + def test_date_tag(self): + date = GLib.Date.new_dmy(7, 1, 2014) + taglist = self.make_taglist(Gst.TAG_DATE, [date]) + + result = tags.convert_taglist(taglist) + + assert isinstance(result[Gst.TAG_DATE][0], compat.text_type) + assert result[Gst.TAG_DATE][0] == '2014-01-07' + 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 9657004b7705e69b317b3482d688921c3b4df44c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 02:15:10 +0100 Subject: [PATCH 76/92] audio: Move date tag from Track to Album The Track model doesn't have a date attribute. --- mopidy/audio/tags.py | 2 +- tests/audio/test_tags.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 78c09775..bdf58600 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -106,7 +106,7 @@ def convert_tags_to_track(tags): album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0] album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] - track_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] + album_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 4619273b..6dfa909d 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -111,10 +111,11 @@ class TagsToTrackTest(unittest.TestCase): albumartist = Artist(name='albumartist', musicbrainz_id='albumartistid') - album = Album(name='album', num_tracks=2, num_discs=3, + album = Album(name='album', date='2006-01-01', + num_tracks=2, num_discs=3, musicbrainz_id='albumid', artists=[albumartist]) - self.track = Track(name='track', date='2006-01-01', + self.track = Track(name='track', genre='genre', track_no=1, disc_no=2, comment='comment', musicbrainz_id='trackid', album=album, bitrate=1000, artists=[artist], @@ -177,7 +178,8 @@ class TagsToTrackTest(unittest.TestCase): def test_missing_track_date(self): del self.tags['date'] - self.check(self.track.replace(date=None)) + self.check( + self.track.replace(album=self.track.album.replace(date=None))) def test_multiple_track_date(self): self.tags['date'].append('2030-01-01') From e68c4668fec489d5d35732715a8fbfb75403b916 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 02:15:45 +0100 Subject: [PATCH 77/92] audio: Fallback to datetime tag if no date tag --- mopidy/audio/tags.py | 4 ++++ tests/audio/test_tags.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index bdf58600..79ab346c 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -107,6 +107,10 @@ def convert_tags_to_track(tags): album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0] album_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0] + if not album_kwargs['date']: + datetime = tags.get(Gst.TAG_DATE_TIME, [None])[0] + if datetime is not None: + album_kwargs['date'] = datetime.split('T')[0] # Clear out any empty values we found track_kwargs = {k: v for k, v in track_kwargs.items() if v} diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 6dfa909d..6a838a27 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -185,6 +185,11 @@ class TagsToTrackTest(unittest.TestCase): self.tags['date'].append('2030-01-01') self.check(self.track) + def test_datetime_instead_of_date(self): + del self.tags['date'] + self.tags['datetime'] = ['2006-01-01T14:13:12Z'] + self.check(self.track) + def test_missing_track_comment(self): del self.tags['comment'] self.check(self.track.replace(comment=None)) From df6db63dd4069c22e75b8599353882e27cae4643 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 18 Dec 2015 23:47:17 +0100 Subject: [PATCH 78/92] gst1: Remove clearified TODO --- mopidy/audio/actor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index ca25f4dd..160dc8a8 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -249,7 +249,6 @@ class _Handler(object): # XXX: We're not called on the last state change when going down to # NULL, so we rewrite the second to last call to get the expected # behavior. - # TODO/Gst1: Is this workaround still needed? new_state = Gst.State.NULL pending_state = Gst.State.VOID_PENDING From 190abc3513e5af7d9d9cf0746d6595e94c48aab2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jan 2016 23:29:24 +0100 Subject: [PATCH 79/92] gst1: Use default queue settings Removing this queue seems to break appsrc about to finish. --- mopidy/audio/actor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 160dc8a8..92ccb44a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -464,12 +464,10 @@ class Audio(pykka.ThreadingActor): # Queue element to buy us time between the about-to-finish event and # the actual switch, i.e. about to switch can block for longer thanks # to this queue. - # TODO: make the min-max values a setting? + # TODO: 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') - queue.set_property('max-size-buffers', 0) - queue.set_property('max-size-bytes', 0) - queue.set_property('max-size-time', 3 * Gst.SECOND) - queue.set_property('min-threshold-time', 1 * Gst.SECOND) audio_sink.add(queue) audio_sink.add(self._outputs) From 3cf8cdb3d97ee5a77dbb794d623505a4383d3312 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Jan 2016 21:49:19 +0100 Subject: [PATCH 80/92] travis: Add gstreamer1.0-plugins-bad to deps --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2acbf87e..f46d5ae2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: before_install: - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573 - "sudo apt-get update -qq" - - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good python-gst-1.0" + - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad python-gst-1.0" install: - "pip install tox" From 1c4b36f66aa7414a29bb4e55d1efeda3e66bd523 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 13:05:52 +0100 Subject: [PATCH 81/92] gst1: gi.require_version() GstPbutils before importing it --- mopidy/audio/actor.py | 1 + mopidy/audio/scan.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 92ccb44a..bb08eb5a 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -6,6 +6,7 @@ import threading import gi gi.require_version('Gst', '1.0') +gi.require_version('GstPbutils', '1.0') from gi.repository import GObject, Gst, GstPbutils Gst.is_initialized() or Gst.init() diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index ed1c6424..0ed26401 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -6,6 +6,7 @@ import time import gi gi.require_version('Gst', '1.0') +gi.require_version('GstPbutils', '1.0') from gi.repository import Gst, GstPbutils Gst.is_initialized() or Gst.init() From 906a48eaf7d7f21f80fcf3f9f7b1590aff815656 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 13:14:43 +0100 Subject: [PATCH 82/92] gst1: Fix digraph name It was probably broken by some regexp replacement. --- mopidy/audio/actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index bb08eb5a..6f444758 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -696,7 +696,7 @@ class Audio(pykka.ThreadingActor): """ Internal method for setting the raw GStreamer state. - .. digraph:: Gst.State.transitions + .. digraph:: gst_state_transitions graph [rankdir="LR"]; node [fontsize=10]; From dce7e1551d654ae8471a1c09f1426a0f8a1336f4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 13:37:16 +0100 Subject: [PATCH 83/92] gst1: Simplify Gentoo install docs --- docs/installation/source.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index d9994c6b..b4b7ad3f 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -60,7 +60,7 @@ please follow the directions :ref:`here `. If you use Gentoo you can install GStreamer like this:: - emerge -av gst-python gst-plugins-good gst-plugins-ugly gst-plugins-meta + emerge -av gst-python gst-plugins-meta ``gst-plugins-meta`` is the one that actually pulls in the plugins you want, so pay attention to the USE flags, e.g. ``alsa``, ``mp3``, etc. From b143898cd3236ded1ba517b2346cbfa748d03884 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 20:27:36 +0100 Subject: [PATCH 84/92] gst1: Adjust list of GStreamer packages needed on Arch --- docs/installation/source.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/installation/source.rst b/docs/installation/source.rst index b4b7ad3f..ed738dda 100644 --- a/docs/installation/source.rst +++ b/docs/installation/source.rst @@ -50,11 +50,18 @@ please follow the directions :ref:`here `. If you use Arch Linux, install the following packages from the official repository:: - sudo pacman -S gst-python gst-plugins-good gst-plugins-ugly + sudo pacman -S python2-gobject gst-python gst-plugins-good + gst-plugins-ugly + + .. warning:: + + ``gst-python`` installs GStreamer GI overrides for Python 3. As far as + we know, Arch currently lacks a package with the corresponding overrides + built for Python 2. If a ``gst-python2`` package is added, it will + depend on ``python2-gobject``, so we can then shorten this package list. If you use Fedora you can install GStreamer like this:: - # TODO Update to GStreamer 1 sudo yum install -y python-gstreamer1 gstreamer1-plugins-good \ gstreamer1-plugins-ugly From d9f53d5da3c059a2dad97d6bdb163d92cb2c6db4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 23:06:45 +0100 Subject: [PATCH 85/92] gst1: Move all gi imports to a helper module --- mopidy/__main__.py | 20 +------------------- mopidy/audio/actor.py | 7 +------ mopidy/audio/scan.py | 7 +------ mopidy/audio/tags.py | 6 +----- mopidy/audio/utils.py | 5 +---- mopidy/internal/deps.py | 6 +----- mopidy/internal/gi.py | 33 +++++++++++++++++++++++++++++++++ tests/audio/test_actor.py | 5 +---- tests/audio/test_tags.py | 5 +---- tests/audio/test_utils.py | 5 +---- tests/internal/test_deps.py | 5 +---- 11 files changed, 43 insertions(+), 61 deletions(-) create mode 100644 mopidy/internal/gi.py diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 1d9e8314..ee87b82d 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -4,26 +4,8 @@ import logging import os import signal import sys -import textwrap -try: - import gi - gi.require_version('Gst', '1.0') - from gi.repository import Gst -except ImportError: - print(textwrap.dedent(""" - ERROR: The GStreamer Python package was not found. - - Mopidy requires GStreamer to work. GStreamer is a C library with a - number of dependencies itself, and cannot be installed with the regular - Python tools like pip. - - Please see http://docs.mopidy.com/en/latest/installation/ for - instructions on how to install the required dependencies. - """)) - raise -else: - Gst.init() +from mopidy.internal.gi import Gst # noqa: Import to initialize try: # Make GObject's mainloop the event loop for python-dbus diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 6f444758..834bee55 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -4,12 +4,6 @@ import logging import os import threading -import gi -gi.require_version('Gst', '1.0') -gi.require_version('GstPbutils', '1.0') -from gi.repository import GObject, Gst, GstPbutils -Gst.is_initialized() or Gst.init() - import pykka from mopidy import exceptions @@ -17,6 +11,7 @@ from mopidy.audio import tags as tags_lib, utils from mopidy.audio.constants import PlaybackState from mopidy.audio.listener import AudioListener from mopidy.internal import deprecation, process +from mopidy.internal.gi import GObject, Gst, GstPbutils logger = logging.getLogger(__name__) diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0ed26401..0b6831ea 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -4,15 +4,10 @@ from __future__ import ( import collections import time -import gi -gi.require_version('Gst', '1.0') -gi.require_version('GstPbutils', '1.0') -from gi.repository import Gst, GstPbutils -Gst.is_initialized() or Gst.init() - from mopidy import exceptions from mopidy.audio import tags as tags_lib, utils from mopidy.internal import encoding +from mopidy.internal.gi import Gst, GstPbutils # GST_ELEMENT_FACTORY_LIST: _DECODER = 1 << 0 diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py index 79ab346c..62784bc0 100644 --- a/mopidy/audio/tags.py +++ b/mopidy/audio/tags.py @@ -5,13 +5,9 @@ import datetime import logging import numbers -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GLib, Gst -Gst.is_initialized() or Gst.init() - from mopidy import compat from mopidy.internal import log +from mopidy.internal.gi import GLib, Gst from mopidy.models import Album, Artist, Track diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 6a11c7a3..774de53d 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -1,10 +1,7 @@ from __future__ import absolute_import, unicode_literals -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - from mopidy import httpclient +from mopidy.internal.gi import Gst def calculate_duration(num_samples, sample_rate): diff --git a/mopidy/internal/deps.py b/mopidy/internal/deps.py index 8947025f..cc72d371 100644 --- a/mopidy/internal/deps.py +++ b/mopidy/internal/deps.py @@ -5,14 +5,10 @@ import os import platform import sys -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst -Gst.is_initialized() or Gst.init() - import pkg_resources from mopidy.internal import formatting +from mopidy.internal.gi import Gst, gi def format_dependency_list(adapters=None): diff --git a/mopidy/internal/gi.py b/mopidy/internal/gi.py new file mode 100644 index 00000000..16931a90 --- /dev/null +++ b/mopidy/internal/gi.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, unicode_literals + +import textwrap + + +try: + import gi + gi.require_version('Gst', '1.0') + gi.require_version('GstPbutils', '1.0') + from gi.repository import GLib, GObject, Gst, GstPbutils +except ImportError: + print(textwrap.dedent(""" + ERROR: A GObject Python package was not found. + + Mopidy requires GStreamer to work. GStreamer is a C library with a + number of dependencies itself, and cannot be installed with the regular + Python tools like pip. + + Please see http://docs.mopidy.com/en/latest/installation/ for + instructions on how to install the required dependencies. + """)) + raise +else: + Gst.is_initialized() or Gst.init() + + +__all__ = [ + 'GLib', + 'GObject', + 'Gst', + 'GstPbutils', + 'gi', +] diff --git a/tests/audio/test_actor.py b/tests/audio/test_actor.py index 41f730e8..2bcc792a 100644 --- a/tests/audio/test_actor.py +++ b/tests/audio/test_actor.py @@ -3,10 +3,6 @@ from __future__ import absolute_import, unicode_literals import threading import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - import mock import pykka @@ -14,6 +10,7 @@ import pykka from mopidy import audio from mopidy.audio.constants import PlaybackState from mopidy.internal import path +from mopidy.internal.gi import Gst from tests import dummy_audio, path_to_data_dir diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py index 6a838a27..01475124 100644 --- a/tests/audio/test_tags.py +++ b/tests/audio/test_tags.py @@ -4,12 +4,9 @@ from __future__ import absolute_import, unicode_literals import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GLib, GObject, Gst - from mopidy import compat from mopidy.audio import tags +from mopidy.internal.gi import GLib, GObject, Gst from mopidy.models import Album, Artist, Track diff --git a/tests/audio/test_utils.py b/tests/audio/test_utils.py index 0ce15bcb..99c99eb6 100644 --- a/tests/audio/test_utils.py +++ b/tests/audio/test_utils.py @@ -1,12 +1,9 @@ from __future__ import absolute_import, unicode_literals -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - import pytest from mopidy.audio import utils +from mopidy.internal.gi import Gst class TestCreateBuffer(object): diff --git a/tests/internal/test_deps.py b/tests/internal/test_deps.py index ea102b47..84c79d9c 100644 --- a/tests/internal/test_deps.py +++ b/tests/internal/test_deps.py @@ -4,15 +4,12 @@ import platform import sys import unittest -import gi -gi.require_version('Gst', '1.0') -from gi.repository import Gst - import mock import pkg_resources from mopidy.internal import deps +from mopidy.internal.gi import Gst, gi class DepsTest(unittest.TestCase): From 1daf5825580d31e3f2825b5b5edfaa2aed8146fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 23:12:16 +0100 Subject: [PATCH 86/92] gst1: Check GStreamer version on start If GStreamer is too old, it fails like this: $ mopidy ERROR: Mopidy requires GStreamer >= 1.2, but found GStreamer 1.0.0. --- mopidy/internal/gi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mopidy/internal/gi.py b/mopidy/internal/gi.py index 16931a90..320aa611 100644 --- a/mopidy/internal/gi.py +++ b/mopidy/internal/gi.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, unicode_literals +import sys import textwrap @@ -24,6 +25,14 @@ else: Gst.is_initialized() or Gst.init() +REQUIRED_GST_VERSION = (1, 2) + +if Gst.version() < REQUIRED_GST_VERSION: + sys.exit( + 'ERROR: Mopidy requires GStreamer >= %s, but found %s.' % ( + '.'.join(map(str, REQUIRED_GST_VERSION)), Gst.version_string())) + + __all__ = [ 'GLib', 'GObject', From eda91cfa962668e5c10059b0ae487fa8066462ec Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Feb 2016 23:27:06 +0100 Subject: [PATCH 87/92] gst1: Add missing __future__ import --- mopidy/internal/gi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/internal/gi.py b/mopidy/internal/gi.py index 320aa611..122d03b8 100644 --- a/mopidy/internal/gi.py +++ b/mopidy/internal/gi.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals import sys import textwrap From af43612630892fc3cc8be9b0f13109b1a89b1198 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 1 Feb 2016 23:58:00 +0100 Subject: [PATCH 88/92] audio: Add a TODO and some notes on duration handling --- mopidy/audio/actor.py | 1 + mopidy/audio/scan.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py index 834bee55..db923e6d 100644 --- a/mopidy/audio/actor.py +++ b/mopidy/audio/actor.py @@ -746,6 +746,7 @@ class Audio(pykka.ThreadingActor): # Default to blank data to trick shoutcast into clearing any previous # values it might have. + # TODO: Verify if this works at all, likely it doesn't. set_value(Gst.TAG_ARTIST, ' ') set_value(Gst.TAG_TITLE, ' ') set_value(Gst.TAG_ALBUM, ' ') diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py index 0b6831ea..c63405b0 100644 --- a/mopidy/audio/scan.py +++ b/mopidy/audio/scan.py @@ -137,6 +137,11 @@ def _start_pipeline(pipeline): 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 From 7df7b9d5f9de766ea0cd9542821a988d13fdcb46 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 10:43:45 +0100 Subject: [PATCH 89/92] gst1: Add Audio API changes to changelog --- docs/changelog.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b8d0ee02..df8405f7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -137,6 +137,25 @@ Audio Because of this change, we can now return years without months or days, which matches the semantics of the date fields in our data models. +- **Breaking:** :meth:`mopidy.audio.Audio.set_appsrc`'s ``caps`` argument has + changed format due to the upgrade from GStreamer 0.10 to GStreamer 1. As + far as we know, this is only used by Mopidy-Spotify. As an example, with + GStreamer 0.10 the Mopidy-Spotify caps was:: + + audio/x-raw-int, endianness=(int)1234, channels=(int)2, width=(int)16, + depth=(int)16, signed=(boolean)true, rate=(int)44100 + + With GStreamer 1 this changes to:: + + audio/x-raw,format=S16LE,rate=44100,channels=2,layout=interleaved + + If you 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. + Gapless ------- From e18ee4798f87e67e063bccddff2d9b81f5da6904 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 15:00:57 +0100 Subject: [PATCH 90/92] gst1: Fix docs typo --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index df8405f7..b7c0bc5a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -149,7 +149,7 @@ Audio audio/x-raw,format=S16LE,rate=44100,channels=2,layout=interleaved - If you Mopidy backend uses ``set_appsrc()``, please refer to GStreamer + 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`` From 00ed7e549c6edc2b4f4d4e8c26ae7a72e4af8e26 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 22:13:58 +0100 Subject: [PATCH 91/92] gst1: Length will always be zero, leave it out --- mopidy/audio/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 774de53d..5f42733d 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -19,8 +19,7 @@ def create_buffer(data, capabilites=None, timestamp=None, duration=None): ``capabilites`` argument is no longer in use """ if not data: - raise ValueError( - 'Cannot create buffer without data: length=%d' % len(data)) + raise ValueError('Cannot create buffer without data') buffer_ = Gst.Buffer.new_wrapped(data) if timestamp is not None: buffer_.pts = timestamp From 673b1b7bdc5b1fad1e196c4436ac9dc2afe10a40 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Feb 2016 22:15:58 +0100 Subject: [PATCH 92/92] gst1: Fix typo in docstring --- mopidy/audio/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/audio/utils.py b/mopidy/audio/utils.py index 5f42733d..8bc5279d 100644 --- a/mopidy/audio/utils.py +++ b/mopidy/audio/utils.py @@ -88,7 +88,7 @@ class Signals(object): self._ids[(element, event)] = element.connect(event, func, *args) def disconnect(self, element, event): - """Disconnect whatever handler we have for and element+event pair. + """Disconnect whatever handler we have for an element+event pair. Does nothing it the handler has already been removed. """