Merge pull request #1419 from mopidy/feature/gst1

Port to GStreamer 1.x and PyGI
This commit is contained in:
Stein Magnus Jodal 2016-02-02 22:24:28 +01:00
commit 3429487f70
29 changed files with 1024 additions and 879 deletions

View File

@ -16,7 +16,7 @@ env:
before_install: before_install:
- "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573 - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573
- "sudo apt-get update -qq" - "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 gstreamer1.0-plugins-bad python-gst-1.0"
install: install:
- "pip install tox" - "pip install tox"

View File

@ -10,6 +10,12 @@ v1.2.0 (UNRELEASED)
Feature release. Feature release.
Dependencies
------------
- Mopidy now requires GStreamer 1.x, as we've finally ported from GStreamer
0.10.
Core API Core API
-------- --------
@ -123,6 +129,33 @@ Cleanups
- Catch errors when loading :confval:`logging/config_file`. - Catch errors when loading :confval:`logging/config_file`.
(Fixes: :issue:`1320`) (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.
- **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 your Mopidy backend uses ``set_appsrc()``, please refer to GStreamer
documentation for details on the new caps string format.
- **Deprecated:** :func:`mopidy.audio.utils.create_buffer`'s ``capabilities``
argument is no longer in use and will be removed in the future. As far as we
know, this is only used by Mopidy-Spotify.
Gapless Gapless
------- -------

View File

@ -37,36 +37,40 @@ please follow the directions :ref:`here <contributing>`.
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
following steps. following steps.
#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python #. Then you'll need to install GStreamer 1.x (>= 1.2), with Python bindings.
bindings. GStreamer is packaged for most popular Linux distributions. Search GStreamer is packaged for most popular Linux distributions. Search for
for GStreamer in your package manager, and make sure to install the Python GStreamer in your package manager, and make sure to install the Python
bindings, and the "good" and "ugly" plugin sets. bindings, and the "good" and "ugly" plugin sets.
If you use Debian/Ubuntu you can install GStreamer like this:: If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ sudo apt-get install python-gst-1.0 gstreamer1.0-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools gstreamer1.0-plugins-ugly gstreamer1.0-tools
If you use Arch Linux, install the following packages from the official If you use Arch Linux, install the following packages from the official
repository:: repository::
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \ sudo pacman -S python2-gobject gst-python gst-plugins-good
gstreamer0.10-ugly-plugins 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:: If you use Fedora you can install GStreamer like this::
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \ sudo yum install -y python-gstreamer1 gstreamer1-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools gstreamer1-plugins-ugly
If you use Gentoo you need to be careful because GStreamer 0.10 is in a If you use Gentoo you can install GStreamer like this::
different lower slot than 1.0, the default. Your emerge commands will need
to include the slot::
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \ emerge -av gst-python gst-plugins-meta
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you ``gst-plugins-meta`` is the one that actually pulls in the plugins you want,
want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc. so pay attention to the USE flags, e.g. ``alsa``, ``mp3``, etc.
#. Install the latest release of Mopidy:: #. Install the latest release of Mopidy::

View File

@ -4,24 +4,8 @@ import logging
import os import os
import signal import signal
import sys import sys
import textwrap
try: from mopidy.internal.gi import Gst # noqa: Import to initialize
import gobject # noqa
except ImportError:
print(textwrap.dedent("""
ERROR: The gobject 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.
Please see http://docs.mopidy.com/en/latest/installation/ for
instructions on how to install the required dependencies.
"""))
raise
gobject.threads_init()
try: try:
# Make GObject's mainloop the event loop for python-dbus # Make GObject's mainloop the event loop for python-dbus
@ -33,13 +17,6 @@ except ImportError:
import pykka.debug 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 import commands, config as config_lib, ext
from mopidy.internal import encoding, log, path, process, versioning from mopidy.internal import encoding, log, path, process, versioning
@ -73,7 +50,7 @@ def main():
data.command.set(extension=data.extension) data.command.set(extension=data.extension)
root_cmd.add_child(data.extension.ext_name, data.command) 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( config, config_errors = config_lib.load(
args.config_files, args.config_files,

View File

@ -4,65 +4,28 @@ import logging
import os import os
import threading import threading
import gobject
import pygst
pygst.require('0.10')
import gst # noqa
import gst.pbutils # noqa
import pykka import pykka
from mopidy import exceptions from mopidy import exceptions
from mopidy.audio import icy, utils from mopidy.audio import tags as tags_lib, utils
from mopidy.audio.constants import PlaybackState from mopidy.audio.constants import PlaybackState
from mopidy.audio.listener import AudioListener from mopidy.audio.listener import AudioListener
from mopidy.internal import deprecation, process from mopidy.internal import deprecation, process
from mopidy.internal.gi import GObject, Gst, GstPbutils
logger = logging.getLogger(__name__) 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 # 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_logger = logging.getLogger('mopidy.audio.gst')
icy.register()
_GST_STATE_MAPPING = { _GST_STATE_MAPPING = {
gst.STATE_PLAYING: PlaybackState.PLAYING, Gst.State.PLAYING: PlaybackState.PLAYING,
gst.STATE_PAUSED: PlaybackState.PAUSED, Gst.State.PAUSED: PlaybackState.PAUSED,
gst.STATE_NULL: PlaybackState.STOPPED} Gst.State.NULL: PlaybackState.STOPPED,
}
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? # TODO: expose this as a property on audio?
@ -71,7 +34,7 @@ class _Appsrc(object):
"""Helper class for dealing with appsrc based playback.""" """Helper class for dealing with appsrc based playback."""
def __init__(self): def __init__(self):
self._signals = _Signals() self._signals = utils.Signals()
self.reset() self.reset()
def reset(self): def reset(self):
@ -120,9 +83,11 @@ class _Appsrc(object):
if buffer_ is None: if buffer_ is None:
gst_logger.debug('Sending appsrc end-of-stream event.') 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: 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): def _on_signal(self, element, clocktime, func):
# This shim is used to ensure we always return true, and also handles # This shim is used to ensure we always return true, and also handles
@ -135,29 +100,30 @@ class _Appsrc(object):
# TODO: expose this as a property on audio when #790 gets further along. # TODO: expose this as a property on audio when #790 gets further along.
class _Outputs(gst.Bin): class _Outputs(Gst.Bin):
def __init__(self): 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.element_factory_make('tee') self._tee = Gst.ElementFactory.make('tee')
self.add(self._tee) self.add(self._tee)
ghost_pad = gst.GhostPad('sink', self._tee.get_pad('sink')) ghost_pad = Gst.GhostPad.new('sink', self._tee.get_static_pad('sink'))
self.add_pad(ghost_pad) self.add_pad(ghost_pad)
# Add an always connected fakesink which respects the clock so the tee # Add an always connected fakesink which respects the clock so the tee
# doesn't fail even if we don't have any outputs. # 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) fakesink.set_property('sync', True)
self._add(fakesink) self._add(fakesink)
def add_output(self, description): def add_output(self, description):
# XXX This only works for pipelines not in use until #790 gets done. # XXX This only works for pipelines not in use until #790 gets done.
try: try:
output = gst.parse_bin_from_description( output = Gst.parse_bin_from_description(
description, ghost_unconnected_pads=True) description, ghost_unlinked_pads=True)
except gobject.GError as ex: except GObject.GError as ex:
logger.error( logger.error(
'Failed to create audio output "%s": %s', description, ex) 'Failed to create audio output "%s": %s', description, ex)
raise exceptions.AudioException(bytes(ex)) raise exceptions.AudioException(bytes(ex))
@ -166,7 +132,7 @@ class _Outputs(gst.Bin):
logger.info('Audio output set to "%s"', description) logger.info('Audio output set to "%s"', description)
def _add(self, element): def _add(self, element):
queue = gst.element_factory_make('queue') queue = Gst.ElementFactory.make('queue')
self.add(element) self.add(element)
self.add(queue) self.add(queue)
queue.link(element) queue.link(element)
@ -181,7 +147,7 @@ class SoftwareMixer(object):
self._element = None self._element = None
self._last_volume = None self._last_volume = None
self._last_mute = None self._last_mute = None
self._signals = _Signals() self._signals = utils.Signals()
def setup(self, element, mixer_ref): def setup(self, element, mixer_ref):
self._element = element self._element = element
@ -223,7 +189,8 @@ class _Handler(object):
def setup_event_handling(self, pad): def setup_event_handling(self, pad):
self._pad = 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_pad_event)
def teardown_message_handling(self): def teardown_message_handling(self):
bus = self._element.get_bus() bus = self._element.get_bus()
@ -232,55 +199,59 @@ class _Handler(object):
self._message_handler_id = None self._message_handler_id = None
def teardown_event_handling(self): 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 self._event_handler_id = None
def on_message(self, bus, msg): 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:
self.on_playbin_state_changed(*msg.parse_state_changed()) if msg.src != self._element:
elif msg.type == gst.MESSAGE_BUFFERING: return
self.on_buffering(msg.parse_buffering(), msg.structure) old_state, new_state, pending_state = msg.parse_state_changed()
elif msg.type == gst.MESSAGE_EOS: 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() self.on_end_of_stream()
elif msg.type == gst.MESSAGE_ERROR: elif msg.type == Gst.MessageType.ERROR:
self.on_error(*msg.parse_error()) error, debug = msg.parse_error()
elif msg.type == gst.MESSAGE_WARNING: self.on_error(error, debug)
self.on_warning(*msg.parse_warning()) elif msg.type == Gst.MessageType.WARNING:
elif msg.type == gst.MESSAGE_ASYNC_DONE: error, debug = msg.parse_warning()
self.on_warning(error, debug)
elif msg.type == Gst.MessageType.ASYNC_DONE:
self.on_async_done() self.on_async_done()
elif msg.type == gst.MESSAGE_TAG: elif msg.type == Gst.MessageType.TAG:
self.on_tag(msg.parse_tag()) taglist = msg.parse_tag()
elif msg.type == gst.MESSAGE_ELEMENT: self.on_tag(taglist)
if gst.pbutils.is_missing_plugin_message(msg): elif msg.type == Gst.MessageType.ELEMENT:
if GstPbutils.is_missing_plugin_message(msg):
self.on_missing_plugin(msg) self.on_missing_plugin(msg)
elif msg.type == Gst.MessageType.STREAM_START:
self.on_stream_start()
def on_event(self, pad, event): def on_pad_event(self, pad, pad_probe_info):
if event.type == gst.EVENT_NEWSEGMENT: event = pad_probe_info.get_event()
self.on_new_segment(*event.parse_new_segment()) if event.type == Gst.EventType.SEGMENT:
elif event.type == gst.EVENT_SINK_MESSAGE: self.on_segment(event.parse_segment())
# Handle stream changed messages when they reach our output bin. return Gst.PadProbeReturn.OK
# 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'):
self.on_stream_changed(msg.structure['uri'])
return True
def on_playbin_state_changed(self, old_state, new_state, pending_state): 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', gst_logger.debug(
old_state.value_name, new_state.value_name, 'Got STATE_CHANGED bus message: old=%s new=%s pending=%s',
pending_state.value_name) 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 # 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 # NULL, so we rewrite the second to last call to get the expected
# behavior. # behavior.
new_state = gst.STATE_NULL new_state = Gst.State.NULL
pending_state = gst.STATE_VOID_PENDING 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 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 return # Ignore READY state as it's GStreamer specific
new_state = _GST_STATE_MAPPING[new_state] new_state = _GST_STATE_MAPPING[new_state]
@ -299,80 +270,96 @@ class _Handler(object):
AudioListener.send('stream_changed', uri=None) AudioListener.send('stream_changed', uri=None)
if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ: if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ:
gst.DEBUG_BIN_TO_DOT_FILE( Gst.debug_bin_to_dot_file(
self._audio._playbin, gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') self._audio._playbin, Gst.DebugGraphDetails.ALL, 'mopidy')
def on_buffering(self, percent, structure=None): def on_buffering(self, percent, structure=None):
if structure and structure.has_field('buffering-mode'): if structure is not None and structure.has_field('buffering-mode'):
if structure['buffering-mode'] == gst.BUFFERING_LIVE: buffering_mode = structure.get_enum(
'buffering-mode', Gst.BufferingMode)
if buffering_mode == Gst.BufferingMode.LIVE:
return # Live sources stall in paused. return # Live sources stall in paused.
level = logging.getLevelName('TRACE') level = logging.getLevelName('TRACE')
if percent < 10 and not self._audio._buffering: 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 self._audio._buffering = True
level = logging.DEBUG level = logging.DEBUG
if percent == 100: if percent == 100:
self._audio._buffering = False self._audio._buffering = False
if self._audio._target_state == gst.STATE_PLAYING: if self._audio._target_state == Gst.State.PLAYING:
self._audio._playbin.set_state(gst.STATE_PLAYING) self._audio._playbin.set_state(Gst.State.PLAYING)
level = logging.DEBUG 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): 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()') logger.debug('Audio event: reached_end_of_stream()')
self._audio._tags = {} self._audio._tags = {}
AudioListener.send('reached_end_of_stream') AudioListener.send('reached_end_of_stream')
def on_error(self, error, debug): def on_error(self, error, debug):
gst_logger.error(str(error).decode('utf-8')) error_msg = str(error).decode('utf-8')
if debug: debug_msg = debug.decode('utf-8')
gst_logger.debug(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? # TODO: is this needed?
self._audio.stop_playback() self._audio.stop_playback()
def on_warning(self, error, debug): def on_warning(self, error, debug):
gst_logger.warning(str(error).decode('utf-8')) error_msg = str(error).decode('utf-8')
if debug: debug_msg = debug.decode('utf-8')
gst_logger.debug(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): def on_async_done(self):
gst_logger.debug('Got async-done.') gst_logger.debug('Got ASYNC_DONE bus message.')
def on_tag(self, taglist): 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) self._audio._tags.update(tags)
logger.debug('Audio event: tags_changed(tags=%r)', tags.keys()) logger.debug('Audio event: tags_changed(tags=%r)', tags.keys())
AudioListener.send('tags_changed', tags=tags.keys()) AudioListener.send('tags_changed', tags=tags.keys())
def on_missing_plugin(self, msg): def on_missing_plugin(self, msg):
desc = gst.pbutils.missing_plugin_message_get_description(msg) desc = GstPbutils.missing_plugin_message_get_description(msg)
debug = gst.pbutils.missing_plugin_message_get_installer_detail(msg) debug = GstPbutils.missing_plugin_message_get_installer_detail(msg)
gst_logger.debug(
gst_logger.debug('Got missing-plugin message: description:%s', desc) 'Got missing-plugin bus message: description=%r', desc)
logger.warning('Could not find a %s to handle media.', 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: ' logger.info('You might be able to fix this by running: '
'gst-installer "%s"', debug) 'gst-installer "%s"', debug)
# TODO: store the missing plugins installer info in a file so we can # TODO: store the missing plugins installer info in a file so we can
# can provide a 'mopidy install-missing-plugins' if the system has the # can provide a 'mopidy install-missing-plugins' if the system has the
# required helper installed? # required helper installed?
def on_new_segment(self, update, rate, format_, start, stop, position): def on_stream_start(self):
gst_logger.debug('Got new-segment event: update=%s rate=%s format=%s ' gst_logger.debug('Got STREAM_START bus message')
'start=%s stop=%s position=%s', update, rate, uri = self._audio._pending_uri
format_.value_name, start, stop, position) logger.debug('Audio event: stream_changed(uri=%r)', uri)
position_ms = position // gst.MSECOND
logger.debug('Audio event: position_changed(position=%s)', 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) AudioListener.send('stream_changed', uri=uri)
def on_segment(self, segment):
gst_logger.debug(
'Got SEGMENT pad 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=%r)', position_ms)
AudioListener.send('position_changed', position=position_ms)
# TODO: create a player class which replaces the actors internals # TODO: create a player class which replaces the actors internals
class Audio(pykka.ThreadingActor): class Audio(pykka.ThreadingActor):
@ -391,9 +378,10 @@ class Audio(pykka.ThreadingActor):
super(Audio, self).__init__() super(Audio, self).__init__()
self._config = config self._config = config
self._target_state = gst.STATE_NULL self._target_state = Gst.State.NULL
self._buffering = False self._buffering = False
self._tags = {} self._tags = {}
self._pending_uri = None
self._playbin = None self._playbin = None
self._outputs = None self._outputs = None
@ -401,7 +389,7 @@ class Audio(pykka.ThreadingActor):
self._handler = _Handler(self) self._handler = _Handler(self)
self._appsrc = _Appsrc() self._appsrc = _Appsrc()
self._signals = _Signals() self._signals = utils.Signals()
if mixer and self._config['audio']['mixer'] == 'software': if mixer and self._config['audio']['mixer'] == 'software':
self.mixer = SoftwareMixer(mixer) self.mixer = SoftwareMixer(mixer)
@ -413,7 +401,7 @@ class Audio(pykka.ThreadingActor):
self._setup_playbin() self._setup_playbin()
self._setup_outputs() self._setup_outputs()
self._setup_audio_sink() self._setup_audio_sink()
except gobject.GError as ex: except GObject.GError as ex:
logger.exception(ex) logger.exception(ex)
process.exit_process() process.exit_process()
@ -424,19 +412,18 @@ class Audio(pykka.ThreadingActor):
def _setup_preferences(self): def _setup_preferences(self):
# TODO: move out of audio actor? # TODO: move out of audio actor?
# Fix for https://github.com/mopidy/mopidy/issues/604 # Fix for https://github.com/mopidy/mopidy/issues/604
registry = gst.registry_get_default() registry = Gst.Registry.get()
jacksink = registry.find_feature( jacksink = registry.find_feature('jackaudiosink', Gst.ElementFactory)
'jackaudiosink', gst.TYPE_ELEMENT_FACTORY)
if jacksink: if jacksink:
jacksink.set_rank(gst.RANK_SECONDARY) jacksink.set_rank(Gst.Rank.SECONDARY)
def _setup_playbin(self): def _setup_playbin(self):
playbin = gst.element_factory_make('playbin2') playbin = Gst.ElementFactory.make('playbin')
playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO
# TODO: turn into config values... # TODO: turn into config values...
playbin.set_property('buffer-size', 5 << 20) # 5MB 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, 'source-setup', self._on_source_setup)
self._signals.connect(playbin, 'about-to-finish', self._signals.connect(playbin, 'about-to-finish',
@ -450,13 +437,13 @@ class Audio(pykka.ThreadingActor):
self._handler.teardown_event_handling() self._handler.teardown_event_handling()
self._signals.disconnect(self._playbin, 'about-to-finish') self._signals.disconnect(self._playbin, 'about-to-finish')
self._signals.disconnect(self._playbin, 'source-setup') 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): def _setup_outputs(self):
# We don't want to use outputs for regular testing, so just install # We don't want to use outputs for regular testing, so just install
# an unsynced fakesink when someone asks for a 'testoutput'. # an unsynced fakesink when someone asks for a 'testoutput'.
if self._config['audio']['output'] == 'testoutput': if self._config['audio']['output'] == 'testoutput':
self._outputs = gst.element_factory_make('fakesink') self._outputs = Gst.ElementFactory.make('fakesink')
else: else:
self._outputs = _Outputs() self._outputs = _Outputs()
try: try:
@ -464,26 +451,25 @@ class Audio(pykka.ThreadingActor):
except exceptions.AudioException: except exceptions.AudioException:
process.exit_process() # TODO: move this up the chain 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): 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 # 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 # the actual switch, i.e. about to switch can block for longer thanks
# to this queue. # to this queue.
# TODO: make the min-max values a setting? # TODO: See if settings should be set to minimize latency. Previous
queue = gst.element_factory_make('queue') # setting breaks appsrc, and settings before that broke on a few
queue.set_property('max-size-buffers', 0) # systems. So leave the default to play it safe.
queue.set_property('max-size-bytes', 0) queue = Gst.ElementFactory.make('queue')
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(queue)
audio_sink.add(self._outputs) audio_sink.add(self._outputs)
if self.mixer: if self.mixer:
volume = gst.element_factory_make('volume') volume = Gst.ElementFactory.make('volume')
audio_sink.add(volume) audio_sink.add(volume)
queue.link(volume) queue.link(volume)
volume.link(self._outputs) volume.link(self._outputs)
@ -491,7 +477,7 @@ class Audio(pykka.ThreadingActor):
else: else:
queue.link(self._outputs) queue.link(self._outputs)
ghost_pad = gst.GhostPad('sink', queue.get_pad('sink')) ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink'))
audio_sink.add_pad(ghost_pad) audio_sink.add_pad(ghost_pad)
self._playbin.set_property('audio-sink', audio_sink) self._playbin.set_property('audio-sink', audio_sink)
@ -508,11 +494,12 @@ class Audio(pykka.ThreadingActor):
gst_logger.debug('Got about-to-finish event.') gst_logger.debug('Got about-to-finish event.')
if self._about_to_finish_callback: 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() self._about_to_finish_callback()
def _on_source_setup(self, element, source): 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': if source.get_factory().get_name() == 'appsrc':
self._appsrc.configure(source) self._appsrc.configure(source)
@ -539,6 +526,7 @@ class Audio(pykka.ThreadingActor):
current_volume = None current_volume = None
self._tags = {} # TODO: add test for this somehow self._tags = {} # TODO: add test for this somehow
self._pending_uri = uri
self._playbin.set_property('uri', uri) self._playbin.set_property('uri', uri)
if self.mixer is not None and current_volume is not None: if self.mixer is not None and current_volume is not None:
@ -563,8 +551,10 @@ class Audio(pykka.ThreadingActor):
:type seek_data: callable which takes time position in ms :type seek_data: callable which takes time position in ms
""" """
self._appsrc.prepare( 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://') uri = 'appsrc://'
self._pending_uri = uri
self._playbin.set_property('uri', uri)
def emit_data(self, buffer_): def emit_data(self, buffer_):
""" """
@ -579,7 +569,7 @@ class Audio(pykka.ThreadingActor):
Returns :class:`True` if data was delivered. Returns :class:`True` if data was delivered.
:param buffer_: buffer to pass to appsrc :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 :rtype: boolean
""" """
return self._appsrc.push(buffer_) return self._appsrc.push(buffer_)
@ -617,15 +607,16 @@ class Audio(pykka.ThreadingActor):
:rtype: int :rtype: int
""" """
try: success, position = self._playbin.query_position(Gst.Format.TIME)
gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0]
return utils.clocktime_to_millisecond(gst_position) if not success:
except gst.QueryError:
# TODO: take state into account for this and possibly also return # TODO: take state into account for this and possibly also return
# None as the unknown value instead of zero? # None as the unknown value instead of zero?
logger.debug('Position query failed') logger.debug('Position query failed')
return 0 return 0
return utils.clocktime_to_millisecond(position)
def set_position(self, position): def set_position(self, position):
""" """
Set position in milliseconds. Set position in milliseconds.
@ -636,9 +627,9 @@ class Audio(pykka.ThreadingActor):
""" """
# TODO: double check seek flags in use. # TODO: double check seek flags in use.
gst_position = utils.millisecond_to_clocktime(position) gst_position = utils.millisecond_to_clocktime(position)
gst_logger.debug('Sending flushing seek: position=%r', gst_position)
result = self._playbin.seek_simple( result = self._playbin.seek_simple(
gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position) Gst.Format.TIME, Gst.SeekFlags.FLUSH, gst_position)
gst_logger.debug('Sent flushing seek: position=%s', gst_position)
return result return result
def start_playback(self): def start_playback(self):
@ -647,7 +638,7 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False` :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): def pause_playback(self):
""" """
@ -655,7 +646,7 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False` :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): def prepare_change(self):
""" """
@ -664,9 +655,9 @@ class Audio(pykka.ThreadingActor):
This function *MUST* be called before changing URIs or doing This function *MUST* be called before changing URIs or doing
changes like updating data that is being pushed. The reason for this changes like updating data that is being pushed. The reason for this
is that GStreamer will reset all its state when it changes to 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): def stop_playback(self):
""" """
@ -675,14 +666,14 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False` :rtype: :class:`True` if successfull, else :class:`False`
""" """
self._buffering = False self._buffering = False
return self._set_state(gst.STATE_NULL) return self._set_state(Gst.State.NULL)
def wait_for_state_change(self): def wait_for_state_change(self):
"""Block until any pending state changes are complete. """Block until any pending state changes are complete.
Should only be used by tests. Should only be used by tests.
""" """
self._playbin.get_state() self._playbin.get_state(timeout=Gst.CLOCK_TIME_NONE)
def enable_sync_handler(self): def enable_sync_handler(self):
"""Enable manual processing of messages from bus. """Enable manual processing of messages from bus.
@ -691,7 +682,7 @@ class Audio(pykka.ThreadingActor):
""" """
def sync_handler(bus, message): def sync_handler(bus, message):
self._handler.on_message(bus, message) self._handler.on_message(bus, message)
return gst.BUS_DROP return Gst.BusSyncReply.DROP
bus = self._playbin.get_bus() bus = self._playbin.get_bus()
bus.set_sync_handler(sync_handler) bus.set_sync_handler(sync_handler)
@ -712,17 +703,18 @@ class Audio(pykka.ThreadingActor):
"READY" -> "NULL" "READY" -> "NULL"
"READY" -> "PAUSED" "READY" -> "PAUSED"
:param state: State to set playbin to. One of: `gst.STATE_NULL`, :param state: State to set playbin to. One of: `Gst.State.NULL`,
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. `Gst.State.READY`, `Gst.State.PAUSED` and `Gst.State.PLAYING`.
:type state: :class:`gst.State` :type state: :class:`Gst.State`
:rtype: :class:`True` if successfull, else :class:`False` :rtype: :class:`True` if successfull, else :class:`False`
""" """
self._target_state = state self._target_state = state
result = self._playbin.set_state(state) result = self._playbin.set_state(state)
gst_logger.debug('State change to %s: result=%s', state.value_name, gst_logger.debug(
result.value_name) 'Changing state to %s: result=%s', state.value_name,
result.value_name)
if result == gst.STATE_CHANGE_FAILURE: if result == Gst.StateChangeReturn.FAILURE:
logger.warning( logger.warning(
'Setting GStreamer state to %s failed', state.value_name) 'Setting GStreamer state to %s failed', state.value_name)
return False return False
@ -735,35 +727,45 @@ class Audio(pykka.ThreadingActor):
""" """
Set track metadata for currently playing song. 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 already inject tags in playbin, e.g. when using :meth:`emit_data` to
deliver raw audio data to GStreamer. deliver raw audio data to GStreamer.
:param track: the current track :param track: the current track
:type track: :class:`mopidy.models.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] 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 # Default to blank data to trick shoutcast into clearing any previous
# values it might have. # values it might have.
taglist[gst.TAG_ARTIST] = ' ' # TODO: Verify if this works at all, likely it doesn't.
taglist[gst.TAG_TITLE] = ' ' set_value(Gst.TAG_ARTIST, ' ')
taglist[gst.TAG_ALBUM] = ' ' set_value(Gst.TAG_TITLE, ' ')
set_value(Gst.TAG_ALBUM, ' ')
if artists: 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: if track.name:
taglist[gst.TAG_TITLE] = track.name set_value(Gst.TAG_TITLE, track.name)
if track.album and track.album.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) 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? # TODO: check if we get this back on our own bus?
self._playbin.send_event(event) self._playbin.send_event(event)
gst_logger.debug('Sent tag event: track=%s', track.uri)
def get_current_tags(self): def get_current_tags(self):
""" """

View File

@ -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)

View File

@ -2,21 +2,27 @@ from __future__ import (
absolute_import, division, print_function, unicode_literals) absolute_import, division, print_function, unicode_literals)
import collections import collections
import time
import pygst
pygst.require('0.10')
import gst # noqa
import gst.pbutils # noqa
from mopidy import exceptions from mopidy import exceptions
from mopidy.audio import utils from mopidy.audio import tags as tags_lib, utils
from mopidy.internal import encoding from mopidy.internal import encoding
from mopidy.internal.gi import Gst, GstPbutils
# 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 = collections.namedtuple(
'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) '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)? # TODO: replace with a scan(uri, timeout=1000, proxy_config=None)?
class Scanner(object): class Scanner(object):
@ -51,7 +57,7 @@ class Scanner(object):
""" """
timeout = int(timeout or self._timeout_ms) timeout = int(timeout or self._timeout_ms)
tags, duration, seekable, mime = None, None, None, None tags, duration, seekable, mime = None, None, None, None
pipeline = _setup_pipeline(uri, self._proxy_config) pipeline, signals = _setup_pipeline(uri, self._proxy_config)
try: try:
_start_pipeline(pipeline) _start_pipeline(pipeline)
@ -59,7 +65,8 @@ class Scanner(object):
duration = _query_duration(pipeline) duration = _query_duration(pipeline)
seekable = _query_seekable(pipeline) seekable = _query_seekable(pipeline)
finally: finally:
pipeline.set_state(gst.STATE_NULL) signals.clear()
pipeline.set_state(Gst.State.NULL)
del pipeline del pipeline
return _Result(uri, tags, duration, seekable, mime, have_audio) return _Result(uri, tags, duration, seekable, mime, have_audio)
@ -68,117 +75,149 @@ class Scanner(object):
# Turns out it's _much_ faster to just create a new pipeline for every as # 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. # decodebins and other elements don't seem to take well to being reused.
def _setup_pipeline(uri, proxy_config=None): 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: if not src:
raise exceptions.ScannerError('GStreamer can not open: %s' % uri) raise exceptions.ScannerError('GStreamer can not open: %s' % uri)
typefind = gst.element_factory_make('typefind') typefind = Gst.ElementFactory.make('typefind')
decodebin = gst.element_factory_make('decodebin2') decodebin = Gst.ElementFactory.make('decodebin')
pipeline = gst.element_factory_make('pipeline') pipeline = Gst.ElementFactory.make('pipeline')
for e in (src, typefind, decodebin): for e in (src, typefind, decodebin):
pipeline.add(e) pipeline.add(e)
gst.element_link_many(src, typefind, decodebin) src.link(typefind)
typefind.link(decodebin)
if proxy_config: if proxy_config:
utils.setup_proxy(src, proxy_config) utils.setup_proxy(src, proxy_config)
typefind.connect('have-type', _have_type, decodebin) signals = utils.Signals()
decodebin.connect('pad-added', _pad_added, pipeline) 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): def _have_type(element, probability, caps, decodebin):
decodebin.set_property('sink-caps', caps) decodebin.set_property('sink-caps', caps)
struct = gst.Structure('have-type') struct = Gst.Structure.new_empty('have-type')
struct['caps'] = caps.get_structure(0) 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): def _pad_added(element, pad, pipeline):
sink = gst.element_factory_make('fakesink') sink = Gst.ElementFactory.make('fakesink')
sink.set_property('sync', False) sink.set_property('sync', False)
pipeline.add(sink) pipeline.add(sink)
sink.sync_state_with_parent() sink.sync_state_with_parent()
pad.link(sink.get_pad('sink')) pad.link(sink.get_static_pad('sink'))
if pad.get_caps().is_subset(_RAW_AUDIO): if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')):
struct = gst.Structure('have-audio') # Probably won't happen due to autoplug-select fix, but lets play it
element.get_bus().post(gst.message_new_application(element, struct)) # 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): def _start_pipeline(pipeline):
if pipeline.set_state(gst.STATE_PAUSED) == gst.STATE_CHANGE_NO_PREROLL: result = pipeline.set_state(Gst.State.PAUSED)
pipeline.set_state(gst.STATE_PLAYING) if result == Gst.StateChangeReturn.NO_PREROLL:
pipeline.set_state(Gst.State.PLAYING)
def _query_duration(pipeline): def _query_duration(pipeline, timeout=100):
try: # 1. Try and get a duration, return if success.
duration = pipeline.query_duration(gst.FORMAT_TIME, None)[0] # 2. Some formats need to play some buffers before duration is found.
except gst.QueryError: # 3. Wait for a duration change event.
# 4. Try and get a duration again.
success, duration = pipeline.query_duration(Gst.Format.TIME)
if success and duration >= 0:
return duration // Gst.MSECOND
result = pipeline.set_state(Gst.State.PLAYING)
if result == Gst.StateChangeReturn.FAILURE:
return None return None
if duration < 0: gst_timeout = timeout * Gst.MSECOND
return None bus = pipeline.get_bus()
else: bus.timed_pop_filtered(gst_timeout, Gst.MessageType.DURATION_CHANGED)
return duration // gst.MSECOND
success, duration = pipeline.query_duration(Gst.Format.TIME)
if success and duration >= 0:
return duration // Gst.MSECOND
return None
def _query_seekable(pipeline): def _query_seekable(pipeline):
query = gst.query_new_seeking(gst.FORMAT_TIME) query = Gst.Query.new_seeking(Gst.Format.TIME)
pipeline.query(query) pipeline.query(query)
return query.parse_seeking()[1] return query.parse_seeking()[1]
def _process(pipeline, timeout_ms): def _process(pipeline, timeout_ms):
clock = pipeline.get_clock()
bus = pipeline.get_bus() bus = pipeline.get_bus()
timeout = timeout_ms * gst.MSECOND
tags = {} tags = {}
mime = None mime = None
have_audio = False have_audio = False
missing_message = None missing_message = None
types = ( types = (
gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR | Gst.MessageType.ELEMENT |
gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG) Gst.MessageType.APPLICATION |
Gst.MessageType.ERROR |
Gst.MessageType.EOS |
Gst.MessageType.ASYNC_DONE |
Gst.MessageType.TAG
)
previous = clock.get_time() timeout = timeout_ms
previous = int(time.time() * 1000)
while timeout > 0: while timeout > 0:
message = bus.timed_pop_filtered(timeout, types) message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types)
if message is None: if message is None:
break break
elif message.type == gst.MESSAGE_ELEMENT: elif message.type == Gst.MessageType.ELEMENT:
if gst.pbutils.is_missing_plugin_message(message): if GstPbutils.is_missing_plugin_message(message):
missing_message = message missing_message = message
elif message.type == gst.MESSAGE_APPLICATION: elif message.type == Gst.MessageType.APPLICATION:
if message.structure.get_name() == 'have-type': if message.get_structure().get_name() == 'have-type':
mime = message.structure['caps'].get_name() mime = message.get_structure().get_value('caps').get_name()
if mime.startswith('text/') or mime == 'application/xml': if mime and (
mime.startswith('text/') or mime == 'application/xml'):
return tags, mime, have_audio return tags, mime, have_audio
elif message.structure.get_name() == 'have-audio': elif message.get_structure().get_name() == 'have-audio':
have_audio = True have_audio = True
elif message.type == gst.MESSAGE_ERROR: elif message.type == Gst.MessageType.ERROR:
error = encoding.locale_decode(message.parse_error()[0]) error = encoding.locale_decode(message.parse_error()[0])
if missing_message and not mime: if missing_message and not mime:
caps = missing_message.structure['detail'] caps = missing_message.get_structure().get_value('detail')
mime = caps.get_structure(0).get_name() mime = caps.get_structure(0).get_name()
return tags, mime, have_audio return tags, mime, have_audio
raise exceptions.ScannerError(error) raise exceptions.ScannerError(error)
elif message.type == gst.MESSAGE_EOS: elif message.type == Gst.MessageType.EOS:
return tags, mime, have_audio return tags, mime, have_audio
elif message.type == gst.MESSAGE_ASYNC_DONE: elif message.type == Gst.MessageType.ASYNC_DONE:
if message.src == pipeline: if message.src == pipeline:
return tags, mime, have_audio return tags, mime, have_audio
elif message.type == gst.MESSAGE_TAG: elif message.type == Gst.MessageType.TAG:
taglist = message.parse_tag() taglist = message.parse_tag()
# Note that this will only keep the last tag. # Note that this will only keep the last tag.
tags.update(utils.convert_taglist(taglist)) tags.update(tags_lib.convert_taglist(taglist))
now = clock.get_time() now = int(time.time() * 1000)
timeout -= now - previous timeout -= now - previous
previous = now previous = now
@ -189,15 +228,11 @@ if __name__ == '__main__':
import os import os
import sys import sys
import gobject
from mopidy.internal import path from mopidy.internal import path
gobject.threads_init()
scanner = Scanner(5000) scanner = Scanner(5000)
for uri in sys.argv[1:]: 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)) uri = path.path_to_uri(os.path.abspath(uri))
try: try:
result = scanner.scan(uri) result = scanner.scan(uri)

139
mopidy/audio/tags.py Normal file
View File

@ -0,0 +1,139 @@
from __future__ import absolute_import, unicode_literals
import collections
import datetime
import logging
import numbers
from mopidy import compat
from mopidy.internal import log
from mopidy.internal.gi import GLib, Gst
from mopidy.models import Album, Artist, Track
logger = logging.getLogger(__name__)
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, 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):
result[tag].append(value.decode('utf-8', 'replace'))
elif isinstance(value, (compat.text_type, bool, numbers.Number)):
result[tag].append(value)
else:
logger.log(
log.TRACE_LOG_LEVEL,
'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]
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}
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]]

View File

@ -1,50 +1,41 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import datetime from mopidy import httpclient
import logging from mopidy.internal.gi import Gst
import numbers
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy import compat, httpclient
from mopidy.models import Album, Artist, Track
logger = logging.getLogger(__name__)
def calculate_duration(num_samples, sample_rate): def calculate_duration(num_samples, sample_rate):
"""Determine duration of samples using GStreamer helper for precise """Determine duration of samples using GStreamer helper for precise
math.""" 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): def create_buffer(data, capabilites=None, timestamp=None, duration=None):
"""Create a new GStreamer buffer based on provided data. """Create a new GStreamer buffer based on provided data.
Mainly intended to keep gst imports out of non-audio modules. 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 not data:
if capabilites: raise ValueError('Cannot create buffer without data')
if isinstance(capabilites, compat.string_types): buffer_ = Gst.Buffer.new_wrapped(data)
capabilites = gst.caps_from_string(capabilites) if timestamp is not None:
buffer_.set_caps(capabilites) buffer_.pts = timestamp
if timestamp: if duration is not None:
buffer_.timestamp = timestamp
if duration:
buffer_.duration = duration buffer_.duration = duration
return buffer_ return buffer_
def millisecond_to_clocktime(value): def millisecond_to_clocktime(value):
"""Convert a millisecond time to internal GStreamer time.""" """Convert a millisecond time to internal GStreamer time."""
return value * gst.MSECOND return value * Gst.MSECOND
def clocktime_to_millisecond(value): def clocktime_to_millisecond(value):
"""Convert an internal GStreamer time to millisecond time.""" """Convert an internal GStreamer time to millisecond time."""
return value // gst.MSECOND return value // Gst.MSECOND
def supported_uri_schemes(uri_schemes): def supported_uri_schemes(uri_schemes):
@ -55,9 +46,9 @@ def supported_uri_schemes(uri_schemes):
:rtype: set of URI schemes we can support via this GStreamer install. :rtype: set of URI schemes we can support via this GStreamer install.
""" """
supported_schemes = set() supported_schemes = set()
registry = gst.registry_get_default() 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(): for uri in factory.get_uri_protocols():
if uri in uri_schemes: if uri in uri_schemes:
supported_schemes.add(uri) supported_schemes.add(uri)
@ -65,84 +56,11 @@ def supported_uri_schemes(uri_schemes):
return supported_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): def setup_proxy(element, config):
"""Configure a GStreamer element with proxy settings. """Configure a GStreamer element with proxy settings.
:param element: element to setup proxy in. :param element: element to setup proxy in.
:type element: :class:`gst.GstElement` :type element: :class:`Gst.GstElement`
:param config: proxy settings to use. :param config: proxy settings to use.
:type config: :class:`dict` :type config: :class:`dict`
""" """
@ -154,51 +72,31 @@ def setup_proxy(element, config):
element.set_property('proxy-pw', config.get('password')) element.set_property('proxy-pw', config.get('password'))
def convert_taglist(taglist): class Signals(object):
"""Convert a :class:`gst.Taglist` to plain Python types.
Knows how to convert: """Helper for tracking gobject signal registrations"""
- Dates def __init__(self):
- Buffers self._ids = {}
- Numbers
- Strings
- Booleans
Unknown types will be ignored and debug logged. Tag keys are all strings def connect(self, element, event, func, *args):
defined as part GStreamer under GstTagList_. """Connect a function + args to signal event on an element.
.. _GstTagList: http://gstreamer.freedesktop.org/data/doc/gstreamer/\ Each event may only be handled by one callback in this implementation.
0.10.36/gstreamer/html/gstreamer-GstTagList.html """
assert (element, event) not in self._ids
self._ids[(element, event)] = element.connect(event, func, *args)
:param taglist: A GStreamer taglist to be converted. def disconnect(self, element, event):
:type taglist: :class:`gst.Taglist` """Disconnect whatever handler we have for an element+event pair.
:rtype: dictionary of tag keys with a list of values.
"""
result = {}
# Taglists are not really dicts, hence the lack of .items() and Does nothing it the handler has already been removed.
# explicit use of .keys() """
for key in taglist.keys(): signal_id = self._ids.pop((element, event), None)
result.setdefault(key, []) if signal_id is not None:
element.disconnect(signal_id)
values = taglist[key] def clear(self):
if not isinstance(values, list): """Clear all registered signal handlers."""
values = [values] for element, event in self._ids.keys():
element.disconnect(self._ids.pop((element, event)))
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)
else:
logger.debug('Ignoring unknown data: %r = %r', key, value)
return result

View File

@ -7,9 +7,7 @@ import logging
import os import os
import sys import sys
import glib from gi.repository import GLib, GObject
import gobject
import pykka import pykka
@ -21,7 +19,7 @@ from mopidy.internal import deps, process, timer, versioning
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_default_config = [] _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.append(os.path.join(base, b'mopidy', b'mopidy.conf'))
DEFAULT_CONFIG = b':'.join(_default_config) DEFAULT_CONFIG = b':'.join(_default_config)
@ -286,7 +284,7 @@ class RootCommand(Command):
help='`section/key=value` values to override config options') help='`section/key=value` values to override config options')
def run(self, args, config): def run(self, args, config):
loop = gobject.MainLoop() loop = GObject.MainLoop()
mixer_class = self.get_mixer_class(config, args.registry['mixer']) mixer_class = self.get_mixer_class(config, args.registry['mixer'])
backend_classes = args.registry['backend'] backend_classes = args.registry['backend']

View File

@ -459,7 +459,7 @@ class PlaybackController(object):
if time_position < 0: if time_position < 0:
time_position = 0 time_position = 0
elif time_position > tl_track.track.length: 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() self.next()
return True return True

View File

@ -7,7 +7,7 @@ import sys
import urllib2 import urllib2
from mopidy import backend, exceptions, models from mopidy import backend, exceptions, models
from mopidy.audio import scan, utils from mopidy.audio import scan, tags
from mopidy.internal import path from mopidy.internal import path
@ -83,7 +83,7 @@ class FileLibraryProvider(backend.LibraryProvider):
try: try:
result = self._scanner.scan(uri) 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) uri=uri, length=result.duration)
except exceptions.ScannerError as e: except exceptions.ScannerError as e:
logger.warning('Failed looking up %s: %s', uri, e) logger.warning('Failed looking up %s: %s', uri, e)

View File

@ -7,11 +7,8 @@ import sys
import pkg_resources import pkg_resources
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy.internal import formatting from mopidy.internal import formatting
from mopidy.internal.gi import Gst, gi
def format_dependency_list(adapters=None): def format_dependency_list(adapters=None):
@ -110,8 +107,7 @@ def pkg_info(project_name=None, include_extras=False):
def gstreamer_info(): def gstreamer_info():
other = [] other = []
other.append('Python wrapper: gst-python %s' % ( other.append('Python wrapper: python-gi %s' % gi.__version__)
'.'.join(map(str, gst.get_pygst_version()))))
found_elements = [] found_elements = []
missing_elements = [] missing_elements = []
@ -135,8 +131,8 @@ def gstreamer_info():
return { return {
'name': 'GStreamer', 'name': 'GStreamer',
'version': '.'.join(map(str, gst.get_gst_version())), 'version': '.'.join(map(str, Gst.version())),
'path': os.path.dirname(gst.__file__), 'path': os.path.dirname(gi.__file__),
'other': '\n'.join(other), 'other': '\n'.join(other),
} }
@ -187,6 +183,6 @@ def _gstreamer_check_elements():
] ]
known_elements = [ known_elements = [
factory.get_name() for factory in factory.get_name() for factory in
gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)] Gst.Registry.get().get_feature_list(Gst.ElementFactory)]
return [ return [
(element, element in known_elements) for element in elements_to_check] (element, element in known_elements) for element in elements_to_check]

42
mopidy/internal/gi.py Normal file
View File

@ -0,0 +1,42 @@
from __future__ import absolute_import, print_function, unicode_literals
import sys
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()
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',
'Gst',
'GstPbutils',
'gi',
]

View File

@ -7,7 +7,7 @@ import socket
import sys import sys
import threading import threading
import gobject from gi.repository import GObject
import pykka import pykka
@ -67,7 +67,7 @@ def format_hostname(hostname):
class Server(object): 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, def __init__(self, host, port, protocol, protocol_kwargs=None,
max_connections=5, timeout=30): max_connections=5, timeout=30):
@ -87,7 +87,7 @@ class Server(object):
return sock return sock
def register_server_socket(self, fileno): 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): def handle_connection(self, fd, flags):
try: try:
@ -132,7 +132,7 @@ class Server(object):
class Connection(object): class Connection(object):
# NOTE: the callback code is _not_ run in the actor's thread, but in the # 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 # 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 # 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 # false return value would only tell us that what we thought was registered
@ -211,14 +211,14 @@ class Connection(object):
return return
self.disable_timeout() self.disable_timeout()
self.timeout_id = gobject.timeout_add_seconds( self.timeout_id = GObject.timeout_add_seconds(
self.timeout, self.timeout_callback) self.timeout, self.timeout_callback)
def disable_timeout(self): def disable_timeout(self):
"""Deactivate timeout mechanism.""" """Deactivate timeout mechanism."""
if self.timeout_id is None: if self.timeout_id is None:
return return
gobject.source_remove(self.timeout_id) GObject.source_remove(self.timeout_id)
self.timeout_id = None self.timeout_id = None
def enable_recv(self): def enable_recv(self):
@ -226,9 +226,9 @@ class Connection(object):
return return
try: try:
self.recv_id = gobject.io_add_watch( self.recv_id = GObject.io_add_watch(
self.sock.fileno(), self.sock.fileno(),
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP,
self.recv_callback) self.recv_callback)
except socket.error as e: except socket.error as e:
self.stop('Problem with connection: %s' % e) self.stop('Problem with connection: %s' % e)
@ -236,7 +236,7 @@ class Connection(object):
def disable_recv(self): def disable_recv(self):
if self.recv_id is None: if self.recv_id is None:
return return
gobject.source_remove(self.recv_id) GObject.source_remove(self.recv_id)
self.recv_id = None self.recv_id = None
def enable_send(self): def enable_send(self):
@ -244,9 +244,9 @@ class Connection(object):
return return
try: try:
self.send_id = gobject.io_add_watch( self.send_id = GObject.io_add_watch(
self.sock.fileno(), self.sock.fileno(),
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP,
self.send_callback) self.send_callback)
except socket.error as e: except socket.error as e:
self.stop('Problem with connection: %s' % e) self.stop('Problem with connection: %s' % e)
@ -255,11 +255,11 @@ class Connection(object):
if self.send_id is None: if self.send_id is None:
return return
gobject.source_remove(self.send_id) GObject.source_remove(self.send_id)
self.send_id = None self.send_id = None
def recv_callback(self, fd, flags): 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) self.stop('Bad client flags: %s' % flags)
return True return True
@ -283,7 +283,7 @@ class Connection(object):
return True return True
def send_callback(self, fd, flags): 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) self.stop('Bad client flags: %s' % flags)
return True return True

View File

@ -2,10 +2,6 @@ from __future__ import absolute_import, unicode_literals
import io import io
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy.compat import configparser from mopidy.compat import configparser
from mopidy.internal import validation from mopidy.internal import validation

View File

@ -4,13 +4,14 @@ import contextlib
import logging import logging
import time import time
from mopidy.internal import log
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TRACE = logging.getLevelName('TRACE')
@contextlib.contextmanager @contextlib.contextmanager
def time_logger(name, level=TRACE): def time_logger(name, level=log.TRACE_LOG_LEVEL):
start = time.time() start = time.time()
yield yield
logger.log(level, '%s took %dms', name, (time.time() - start) * 1000) logger.log(level, '%s took %dms', name, (time.time() - start) * 1000)

View File

@ -6,7 +6,7 @@ import os
import time import time
from mopidy import commands, compat, exceptions 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.internal import path
from mopidy.local import translator from mopidy.local import translator
@ -140,18 +140,18 @@ class ScanCommand(commands.Command):
relpath = translator.local_track_uri_to_path(uri, media_dir) relpath = translator.local_track_uri_to_path(uri, media_dir)
file_uri = path.path_to_uri(os.path.join(media_dir, relpath)) file_uri = path.path_to_uri(os.path.join(media_dir, relpath))
result = scanner.scan(file_uri) result = scanner.scan(file_uri)
tags, duration = result.tags, result.duration
if not result.playable: if not result.playable:
logger.warning('Failed %s: No audio found in file.', uri) 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', logger.warning('Failed %s: Track shorter than %dms',
uri, MIN_DURATION_MS) uri, MIN_DURATION_MS)
else: else:
mtime = file_mtimes.get(os.path.join(media_dir, relpath)) mtime = file_mtimes.get(os.path.join(media_dir, relpath))
track = utils.convert_tags_to_track(tags).replace( track = tags.convert_tags_to_track(result.tags).replace(
uri=uri, length=duration, last_modified=mtime) uri=uri, length=result.duration, last_modified=mtime)
if library.add_supports_tags_and_duration: if library.add_supports_tags_and_duration:
library.add(track, tags=tags, duration=duration) library.add(
track, tags=result.tags, duration=result.duration)
else: else:
library.add(track) library.add(track)
logger.debug('Added %s', track.uri) logger.debug('Added %s', track.uri)

View File

@ -42,11 +42,11 @@ def path_to_local_track_uri(relpath):
URI.""" URI."""
if isinstance(relpath, compat.text_type): if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8') 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): def path_to_local_directory_uri(relpath):
"""Convert path relative to :confval:`local/media_dir` directory URI.""" """Convert path relative to :confval:`local/media_dir` directory URI."""
if isinstance(relpath, compat.text_type): if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8') relpath = relpath.encode('utf-8')
return b'local:directory:%s' % urllib.quote(relpath) return 'local:directory:%s' % urllib.quote(relpath)

View File

@ -8,7 +8,7 @@ import time
import pykka import pykka
from mopidy import audio as audio_lib, backend, exceptions, stream 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.compat import urllib
from mopidy.internal import http, playlists from mopidy.internal import http, playlists
from mopidy.models import Track from mopidy.models import Track
@ -60,7 +60,7 @@ class StreamLibraryProvider(backend.LibraryProvider):
try: try:
result = self._scanner.scan(uri) 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) uri=uri, length=result.duration)
except exceptions.ScannerError as e: except exceptions.ScannerError as e:
logger.warning('Problem looking up %s: %s', uri, e) logger.warning('Problem looking up %s: %s', uri, e)

View File

@ -3,20 +3,14 @@ from __future__ import absolute_import, unicode_literals
import threading import threading
import unittest import unittest
import gobject
gobject.threads_init()
import mock import mock
import pygst
pygst.require('0.10')
import gst # noqa
import pykka import pykka
from mopidy import audio from mopidy import audio
from mopidy.audio.constants import PlaybackState from mopidy.audio.constants import PlaybackState
from mopidy.internal import path from mopidy.internal import path
from mopidy.internal.gi import Gst
from tests import dummy_audio, path_to_data_dir from tests import dummy_audio, path_to_data_dir
@ -520,17 +514,17 @@ class AudioStateTest(unittest.TestCase):
def test_state_does_not_change_when_in_gst_ready_state(self): def test_state_does_not_change_when_in_gst_ready_state(self):
self.audio._handler.on_playbin_state_changed( 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) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
def test_state_changes_from_stopped_to_playing_on_play(self): def test_state_changes_from_stopped_to_playing_on_play(self):
self.audio._handler.on_playbin_state_changed( 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( 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( 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) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state)
@ -538,7 +532,7 @@ class AudioStateTest(unittest.TestCase):
self.audio.state = audio.PlaybackState.PLAYING self.audio.state = audio.PlaybackState.PLAYING
self.audio._handler.on_playbin_state_changed( 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) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state)
@ -546,12 +540,12 @@ class AudioStateTest(unittest.TestCase):
self.audio.state = audio.PlaybackState.PLAYING self.audio.state = audio.PlaybackState.PLAYING
self.audio._handler.on_playbin_state_changed( 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( 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 # We never get the following call, so the logic must work without it
# self.audio._handler.on_playbin_state_changed( # 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) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
@ -565,17 +559,17 @@ class AudioBufferingTest(unittest.TestCase):
def test_pause_when_buffer_empty(self): def test_pause_when_buffer_empty(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.start_playback() 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() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(0) 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) self.assertTrue(self.audio._buffering)
def test_stay_paused_when_buffering_finished(self): def test_stay_paused_when_buffering_finished(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.pause_playback() 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() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(100) self.audio._handler.on_buffering(100)
@ -585,11 +579,11 @@ class AudioBufferingTest(unittest.TestCase):
def test_change_to_paused_while_buffering(self): def test_change_to_paused_while_buffering(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.start_playback() 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() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(0) 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() self.audio.pause_playback()
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
@ -600,13 +594,13 @@ class AudioBufferingTest(unittest.TestCase):
def test_change_to_stopped_while_buffering(self): def test_change_to_stopped_while_buffering(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.start_playback() 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() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(0) 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() playbin.set_state.reset_mock()
self.audio.stop_playback() 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) self.assertFalse(self.audio._buffering)

View File

@ -3,9 +3,6 @@ from __future__ import absolute_import, unicode_literals
import os import os
import unittest import unittest
import gobject
gobject.threads_init()
from mopidy import exceptions from mopidy import exceptions
from mopidy.audio import scan from mopidy.audio import scan
from mopidy.internal import path as path_lib from mopidy.internal import path as path_lib

333
tests/audio/test_tags.py Normal file
View File

@ -0,0 +1,333 @@
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import unittest
from mopidy import compat
from mopidy.audio import tags
from mopidy.internal.gi import GLib, GObject, Gst
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, (GLib.Date, 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_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')
])
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-07T14:13:12Z'
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.
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': ['2006-01-01'],
'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', date='2006-01-01',
num_tracks=2, num_discs=3,
musicbrainz_id='albumid', artists=[albumartist])
self.track = Track(name='track',
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(album=self.track.album.replace(date=None)))
def test_multiple_track_date(self):
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))
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]))

View File

@ -1,261 +1,23 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import datetime import pytest
import unittest
from mopidy.audio import utils from mopidy.audio import utils
from mopidy.models import Album, Artist, Track from mopidy.internal.gi import Gst
# TODO: keep ids without name? class TestCreateBuffer(object):
# 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 def test_creates_buffer(self):
self.tags = { buf = utils.create_buffer(b'123', timestamp=0, duration=1000000)
'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', assert isinstance(buf, Gst.Buffer)
sortname='sortname') assert buf.pts == 0
composer = Artist(name='composer') assert buf.duration == 1000000
performer = Artist(name='performer') assert buf.get_size() == len(b'123')
albumartist = Artist(name='albumartist',
musicbrainz_id='albumartistid')
album = Album(name='album', num_tracks=2, num_discs=3, def test_fails_if_data_has_zero_length(self):
musicbrainz_id='albumid', artists=[albumartist]) with pytest.raises(ValueError) as excinfo:
utils.create_buffer(b'', timestamp=0, duration=1000000)
self.track = Track(name='track', date='2006-01-01', assert 'Cannot create buffer without data' in str(excinfo.value)
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]))

View File

@ -5,7 +5,7 @@ import logging
import socket import socket
import unittest import unittest
import gobject from gi.repository import GObject
from mock import Mock, call, patch, sentinel from mock import Mock, call, patch, sentinel
@ -162,27 +162,27 @@ class ConnectionTest(unittest.TestCase):
network.Connection.stop(self.mock, sentinel.reason) network.Connection.stop(self.mock, sentinel.reason)
network.logger.log(any_int, any_unicode) 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): def test_enable_recv_registers_with_gobject(self):
self.mock.recv_id = None self.mock.recv_id = None
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.sock.fileno.return_value = sentinel.fileno 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) network.Connection.enable_recv(self.mock)
gobject.io_add_watch.assert_called_once_with( GObject.io_add_watch.assert_called_once_with(
sentinel.fileno, 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.mock.recv_callback)
self.assertEqual(sentinel.tag, self.mock.recv_id) 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): def test_enable_recv_already_registered(self):
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.recv_id = sentinel.tag self.mock.recv_id = sentinel.tag
network.Connection.enable_recv(self.mock) 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): def test_enable_recv_does_not_change_tag(self):
self.mock.recv_id = sentinel.tag self.mock.recv_id = sentinel.tag
@ -191,20 +191,20 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_recv(self.mock) network.Connection.enable_recv(self.mock)
self.assertEqual(sentinel.tag, self.mock.recv_id) 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): def test_disable_recv_deregisters(self):
self.mock.recv_id = sentinel.tag self.mock.recv_id = sentinel.tag
network.Connection.disable_recv(self.mock) 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) 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): def test_disable_recv_already_deregistered(self):
self.mock.recv_id = None self.mock.recv_id = None
network.Connection.disable_recv(self.mock) 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) self.assertEqual(None, self.mock.recv_id)
def test_enable_recv_on_closed_socket(self): 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.mock.stop.assert_called_once_with(any_unicode)
self.assertEqual(None, self.mock.recv_id) 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): def test_enable_send_registers_with_gobject(self):
self.mock.send_id = None self.mock.send_id = None
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.sock.fileno.return_value = sentinel.fileno 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) network.Connection.enable_send(self.mock)
gobject.io_add_watch.assert_called_once_with( GObject.io_add_watch.assert_called_once_with(
sentinel.fileno, 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.mock.send_callback)
self.assertEqual(sentinel.tag, self.mock.send_id) 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): def test_enable_send_already_registered(self):
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.send_id = sentinel.tag self.mock.send_id = sentinel.tag
network.Connection.enable_send(self.mock) 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): def test_enable_send_does_not_change_tag(self):
self.mock.send_id = sentinel.tag self.mock.send_id = sentinel.tag
@ -245,20 +245,20 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_send(self.mock) network.Connection.enable_send(self.mock)
self.assertEqual(sentinel.tag, self.mock.send_id) 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): def test_disable_send_deregisters(self):
self.mock.send_id = sentinel.tag self.mock.send_id = sentinel.tag
network.Connection.disable_send(self.mock) 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) 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): def test_disable_send_already_deregistered(self):
self.mock.send_id = None self.mock.send_id = None
network.Connection.disable_send(self.mock) 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) self.assertEqual(None, self.mock.send_id)
def test_enable_send_on_closed_socket(self): def test_enable_send_on_closed_socket(self):
@ -269,36 +269,36 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_send(self.mock) network.Connection.enable_send(self.mock)
self.assertEqual(None, self.mock.send_id) 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): def test_enable_timeout_clears_existing_timeouts(self):
self.mock.timeout = 10 self.mock.timeout = 10
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.mock.disable_timeout.assert_called_once_with() 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): def test_enable_timeout_add_gobject_timeout(self):
self.mock.timeout = 10 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) 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) 10, self.mock.timeout_callback)
self.assertEqual(sentinel.tag, self.mock.timeout_id) 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): def test_enable_timeout_does_not_add_timeout(self):
self.mock.timeout = 0 self.mock.timeout = 0
network.Connection.enable_timeout(self.mock) 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 self.mock.timeout = -1
network.Connection.enable_timeout(self.mock) 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 self.mock.timeout = None
network.Connection.enable_timeout(self.mock) 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): def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self):
self.mock.timeout = 0 self.mock.timeout = 0
@ -313,20 +313,20 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.assertEqual(0, self.mock.disable_timeout.call_count) 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): def test_disable_timeout_deregisters(self):
self.mock.timeout_id = sentinel.tag self.mock.timeout_id = sentinel.tag
network.Connection.disable_timeout(self.mock) 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) 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): def test_disable_timeout_already_deregistered(self):
self.mock.timeout_id = None self.mock.timeout_id = None
network.Connection.disable_timeout(self.mock) 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) self.assertEqual(None, self.mock.timeout_id)
def test_queue_send_acquires_and_releases_lock(self): def test_queue_send_acquires_and_releases_lock(self):
@ -372,7 +372,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup(self): def test_recv_callback_respects_io_hup(self):
@ -380,7 +380,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup_and_io_err(self): 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.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_sends_data_to_actor(self): def test_recv_callback_sends_data_to_actor(self):
@ -398,7 +398,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( 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( self.mock.actor_ref.tell.assert_called_once_with(
{'received': 'data'}) {'received': 'data'})
@ -409,7 +409,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError() self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError()
self.assertTrue(network.Connection.recv_callback( 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_gets_no_data(self): def test_recv_callback_gets_no_data(self):
@ -418,7 +418,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( 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, [ self.assertEqual(self.mock.mock_calls, [
call.sock.recv(any_int), call.sock.recv(any_int),
call.disable_recv(), call.disable_recv(),
@ -431,7 +431,7 @@ class ConnectionTest(unittest.TestCase):
for error in (errno.EWOULDBLOCK, errno.EINTR): for error in (errno.EWOULDBLOCK, errno.EINTR):
self.mock.sock.recv.side_effect = socket.error(error, '') self.mock.sock.recv.side_effect = socket.error(error, '')
self.assertTrue(network.Connection.recv_callback( 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) self.assertEqual(0, self.mock.stop.call_count)
def test_recv_callback_unrecoverable_error(self): def test_recv_callback_unrecoverable_error(self):
@ -439,7 +439,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.sock.recv.side_effect = socket.error self.mock.sock.recv.side_effect = socket.error
self.assertTrue(network.Connection.recv_callback( 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_err(self): def test_send_callback_respects_io_err(self):
@ -450,7 +450,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send_buffer = '' self.mock.send_buffer = ''
self.assertTrue(network.Connection.send_callback( 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup(self): def test_send_callback_respects_io_hup(self):
@ -461,7 +461,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send_buffer = '' self.mock.send_buffer = ''
self.assertTrue(network.Connection.send_callback( 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup_and_io_err(self): 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.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, 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) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_acquires_and_releases_lock(self): 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.mock.sock.send.return_value = 0
self.assertTrue(network.Connection.send_callback( 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.acquire.assert_called_once_with(False)
self.mock.send_lock.release.assert_called_once_with() 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.mock.sock.send.return_value = 0
self.assertTrue(network.Connection.send_callback( 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.acquire.assert_called_once_with(False)
self.assertEqual(0, self.mock.sock.send.call_count) self.assertEqual(0, self.mock.sock.send.call_count)
@ -507,7 +507,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send.return_value = '' self.mock.send.return_value = ''
self.assertTrue(network.Connection.send_callback( 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.disable_send.assert_called_once_with()
self.mock.send.assert_called_once_with('data') self.mock.send.assert_called_once_with('data')
self.assertEqual('', self.mock.send_buffer) self.assertEqual('', self.mock.send_buffer)
@ -519,7 +519,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send.return_value = 'ta' self.mock.send.return_value = 'ta'
self.assertTrue(network.Connection.send_callback( 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.mock.send.assert_called_once_with('data')
self.assertEqual('ta', self.mock.send_buffer) self.assertEqual('ta', self.mock.send_buffer)

View File

@ -4,7 +4,7 @@ import errno
import socket import socket
import unittest import unittest
import gobject from gi.repository import GObject
from mock import Mock, patch, sentinel from mock import Mock, patch, sentinel
@ -91,11 +91,11 @@ class ServerTest(unittest.TestCase):
network.Server.create_server_socket( network.Server.create_server_socket(
self.mock, sentinel.host, sentinel.port) 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): def test_register_server_socket_sets_up_io_watch(self):
network.Server.register_server_socket(self.mock, sentinel.fileno) network.Server.register_server_socket(self.mock, sentinel.fileno)
gobject.io_add_watch.assert_called_once_with( GObject.io_add_watch.assert_called_once_with(
sentinel.fileno, gobject.IO_IN, self.mock.handle_connection) sentinel.fileno, GObject.IO_IN, self.mock.handle_connection)
def test_handle_connection(self): def test_handle_connection(self):
self.mock.accept_connection.return_value = ( self.mock.accept_connection.return_value = (
@ -103,7 +103,7 @@ class ServerTest(unittest.TestCase):
self.mock.maximum_connections_exceeded.return_value = False self.mock.maximum_connections_exceeded.return_value = False
self.assertTrue(network.Server.handle_connection( 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.accept_connection.assert_called_once_with()
self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with()
self.mock.init_connection.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.mock.maximum_connections_exceeded.return_value = True
self.assertTrue(network.Server.handle_connection( 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.accept_connection.assert_called_once_with()
self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with()
self.mock.reject_connection.assert_called_once_with( self.mock.reject_connection.assert_called_once_with(

View File

@ -8,11 +8,8 @@ import mock
import pkg_resources import pkg_resources
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy.internal import deps from mopidy.internal import deps
from mopidy.internal.gi import Gst, gi
class DepsTest(unittest.TestCase): class DepsTest(unittest.TestCase):
@ -74,12 +71,11 @@ class DepsTest(unittest.TestCase):
self.assertEqual('GStreamer', result['name']) self.assertEqual('GStreamer', result['name'])
self.assertEqual( self.assertEqual(
'.'.join(map(str, gst.get_gst_version())), result['version']) '.'.join(map(str, Gst.version())), result['version'])
self.assertIn('gst', result['path']) self.assertIn('gi', result['path'])
self.assertNotIn('__init__.py', result['path']) self.assertNotIn('__init__.py', result['path'])
self.assertIn('Python wrapper: gst-python', result['other']) self.assertIn('Python wrapper: python-gi', result['other'])
self.assertIn( self.assertIn(gi.__version__, result['other'])
'.'.join(map(str, gst.get_pygst_version())), result['other'])
self.assertIn('Relevant elements:', result['other']) self.assertIn('Relevant elements:', result['other'])
@mock.patch('pkg_resources.get_distribution') @mock.patch('pkg_resources.get_distribution')

View File

@ -7,7 +7,7 @@ import shutil
import tempfile import tempfile
import unittest import unittest
import glib from gi.repository import GLib
from mopidy import compat, exceptions from mopidy import compat, exceptions
from mopidy.internal import path from mopidy.internal import path
@ -215,7 +215,7 @@ class ExpandPathTest(unittest.TestCase):
def test_xdg_subsititution(self): def test_xdg_subsititution(self):
self.assertEqual( self.assertEqual(
glib.get_user_data_dir() + b'/foo', GLib.get_user_data_dir() + b'/foo',
path.expand_path(b'$XDG_DATA_DIR/foo')) path.expand_path(b'$XDG_DATA_DIR/foo'))
def test_xdg_subsititution_unknown(self): def test_xdg_subsititution_unknown(self):

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import pytest import pytest
from mopidy import compat
from mopidy.local import translator 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'), (b'\x00\x01\x02', 'local:track:%00%01%02'),
]) ])
def test_path_to_local_track_uri(path, uri): 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', [ @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'), (b'\x00\x01\x02', 'local:directory:%00%01%02'),
]) ])
def test_path_to_local_directory_uri(path, uri): 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