Merge 'develop' and resolve conflict

Conflicts:
	docs/changelog.rst
This commit is contained in:
Jens Lütjen 2016-09-14 16:43:31 +02:00
commit 5057b8b31d
25 changed files with 237 additions and 202 deletions

View File

@ -61,10 +61,6 @@ To get started with Mopidy, check out
:target: https://pypi.python.org/pypi/Mopidy/ :target: https://pypi.python.org/pypi/Mopidy/
:alt: Latest PyPI version :alt: Latest PyPI version
.. image:: https://img.shields.io/pypi/dm/Mopidy.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy/
:alt: Number of PyPI downloads
.. image:: https://img.shields.io/travis/mopidy/mopidy/develop.svg?style=flat .. image:: https://img.shields.io/travis/mopidy/mopidy/develop.svg?style=flat
:target: https://travis-ci.org/mopidy/mopidy :target: https://travis-ci.org/mopidy/mopidy
:alt: Travis CI build status :alt: Travis CI build status

View File

@ -13,8 +13,14 @@ Feature release.
- Core: Mopidy restores its last state when started. Can be enabled by setting - Core: Mopidy restores its last state when started. Can be enabled by setting
the config value :confval:`core/restore_state` to `true`. the config value :confval:`core/restore_state` to `true`.
- MPD: Fix MPD protocol for ``replay_gain_status`` command. The actual command
remains unimplemented. (PR: :issue:`1520`)
v2.0.1 (UNRELEASED) - MPD: Add ``nextsong`` and ``nextsongid`` to the response of MPD ``status`` command.
(Fixes: :issue:`1133`, :issue:`1516`, PR: :issue:`1523`)
v2.0.1 (2016-08-16)
=================== ===================
Bug fix release. Bug fix release.
@ -29,16 +35,48 @@ Bug fix release.
- Audio: Update scan logic to workaround GStreamer issue where tags and - Audio: Update scan logic to workaround GStreamer issue where tags and
duration might only be available after we start playing. duration might only be available after we start playing.
(Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480`, PR: (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474`, :issue:`1480`, PR:
:issue:`1487`) :issue:`1487`)
- Audio: Better handling of seek when position does not match the expected
pending position. (Fixes: :issue:`1462`, :issue:`1505`, PR: :issue:`1496`)
- Audio: Handle bad date tags from audio, thanks to Mario Lang and Tom Parker
who fixed this in parallel. (Fixes: :issue:`1506`, PR: :issue:`1525`,
:issue:`1517`)
- Audio: Make sure scanner handles streams without a duration.
(Fixes: :issue:`1526`)
- Audio: Ensure audio tags are never ``None``. (Fixes: :issue:`1449`)
- Audio: Update :meth:`mopidy.audio.Audio.set_metadata` to postpone sending
tags if there is a pending track change. (Fixes: :issue:`1357`, PR:
:issue:`1538`)
- Core: Avoid endless loop if all tracks in the tracklist are unplayable and - Core: Avoid endless loop if all tracks in the tracklist are unplayable and
consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`) consume mode is off. (Fixes: :issue:`1221`, :issue:`1454`, PR: :issue:`1455`)
- File: Ensure path comparision is done between bytestrings only. Fixes crash - Core: Correctly record the last position of a track when switching to another
one. Particularly relevant for Mopidy-Scrobbler users, as before it was
essentially unusable. (Fixes: :issue:`1456`, PR: :issue:`1534`)
- Models: Fix encoding error if :class:`~mopidy.models.fields.Identifier`
fields, like the ``musicbrainz_id`` model fields, contained non-ASCII Unicode
data. (Fixes: :issue:`1508`, PR: :issue:`1546`)
- File: Ensure path comparison is done between bytestrings only. Fixes crash
where a :confval:`file/media_dirs` path contained non-ASCII characters. where a :confval:`file/media_dirs` path contained non-ASCII characters.
(Fixes: :issue:`1345`, PR: :issue:`1493`) (Fixes: :issue:`1345`, PR: :issue:`1493`)
- Stream: Fix milliseconds vs seconds mistake in timeout handling.
(Fixes: :issue:`1521`, PR: :issue:`1522`)
- Docs: Fix the rendering of :class:`mopidy.core.Core` and
:class:`mopidy.audio.Audio` docs. This should also contribute towards making
the Mopidy Debian package build bit-by-bit reproducible. (Fixes:
:issue:`1500`)
v2.0.0 (2016-02-15) v2.0.0 (2016-02-15)
=================== ===================
@ -378,7 +416,7 @@ Bug fix release.
proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`) proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`)
- Stream: Fix bug in new playlist parser. A non-ASCII char in an urilist - Stream: Fix bug in new playlist parser. A non-ASCII char in an urilist
comment would cause a crash while parsing due to comparision of a non-ASCII comment would cause a crash while parsing due to comparison of a non-ASCII
bytestring with a Unicode string. (Fixes: :issue:`1265`) bytestring with a Unicode string. (Fixes: :issue:`1265`)
- File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real - File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real
@ -2081,10 +2119,10 @@ A release with a number of small and medium fixes, with no specific focus.
- Converted from the optparse to the argparse library for handling command line - Converted from the optparse to the argparse library for handling command line
options. options.
- :option:`mopidy --show-config` will now take into consideration any - ``mopidy --show-config`` will now take into consideration any
:option:`mopidy --option` arguments appearing later on the command line. This :option:`mopidy --option` arguments appearing later on the command line. This
helps you see the effective configuration for runs with the same helps you see the effective configuration for runs with the same
:option:`mopidy --options` arguments. ``mopidy --options`` arguments.
**Audio** **Audio**
@ -2158,7 +2196,7 @@ v0.14.1 (2013-04-28)
==================== ====================
This release addresses an issue in v0.14.0 where the new This release addresses an issue in v0.14.0 where the new
:option:`mopidy-convert-config` tool and the new :option:`mopidy --option` ``mopidy-convert-config`` tool and the new :option:`mopidy --option`
command line option was broken because some string operations inadvertently command line option was broken because some string operations inadvertently
converted some byte strings to unicode. converted some byte strings to unicode.
@ -2188,7 +2226,7 @@ one new.
As part of this change we have cleaned up the naming of our config values. As part of this change we have cleaned up the naming of our config values.
To ease migration we've made a tool named :option:`mopidy-convert-config` for To ease migration we've made a tool named ``mopidy-convert-config`` for
automatically converting the old ``settings.py`` to a new ``mopidy.conf`` automatically converting the old ``settings.py`` to a new ``mopidy.conf``
file. This tool takes care of all the renamed config values as well. See file. This tool takes care of all the renamed config values as well. See
``mopidy-convert-config`` for details on how to use it. ``mopidy-convert-config`` for details on how to use it.
@ -2221,11 +2259,11 @@ one new.
**Command line options** **Command line options**
- The command option :option:`mopidy --list-settings` is now named - The command option ``mopidy --list-settings`` is now named
:option:`mopidy --show-config`. ``mopidy --show-config``.
- The command option :option:`mopidy --list-deps` is now named - The command option ``mopidy --list-deps`` is now named
:option:`mopidy --show-deps`. ``mopidy --show-deps``.
- What configuration files to use can now be specified through the command - What configuration files to use can now be specified through the command
option :option:`mopidy --config`, multiple files can be specified using colon option :option:`mopidy --config`, multiple files can be specified using colon
@ -2235,8 +2273,8 @@ one new.
:option:`mopidy --option`. For example: ``mopidy --option :option:`mopidy --option`. For example: ``mopidy --option
spotify/enabled=false``. spotify/enabled=false``.
- The GStreamer command line options, :option:`mopidy --gst-*` and - The GStreamer command line options, ``mopidy --gst-*`` and
:option:`mopidy --help-gst` are no longer supported. To set GStreamer debug ``mopidy --help-gst`` are no longer supported. To set GStreamer debug
flags, you can use environment variables such as :envvar:`GST_DEBUG`. Refer flags, you can use environment variables such as :envvar:`GST_DEBUG`. Refer
to GStreamer's documentation for details. to GStreamer's documentation for details.
@ -2285,7 +2323,7 @@ already have.
**Core** **Core**
- Removed the :attr:`mopidy.settings.DEBUG_THREAD` setting and the - Removed the :attr:`mopidy.settings.DEBUG_THREAD` setting and the
:option:`--debug-thread` command line option. Sending SIGUSR1 to ``mopidy --debug-thread`` command line option. Sending SIGUSR1 to
the Mopidy process will now always make it log tracebacks for all alive the Mopidy process will now always make it log tracebacks for all alive
threads. threads.
@ -2566,9 +2604,8 @@ We've added an HTTP frontend for those wanting to build web clients for Mopidy!
- Make ``mopidy-scan`` ignore invalid dates, e.g. dates in years outside the - Make ``mopidy-scan`` ignore invalid dates, e.g. dates in years outside the
range 1-9999. range 1-9999.
- Make ``mopidy-scan`` accept :option:`-q`/:option:`--quiet` and - Make ``mopidy-scan`` accept ``-q``/``--quiet`` and ``-v``/``--verbose``
:option:`-v`/:option:`--verbose` options to control the amount of logging options to control the amount of logging output when scanning.
output when scanning.
- The scanner can now handle files with other encodings than UTF-8. Rebuild - The scanner can now handle files with other encodings than UTF-8. Rebuild
your tag cache with ``mopidy-scan`` to include tracks that may have been your tag cache with ``mopidy-scan`` to include tracks that may have been
@ -2693,7 +2730,7 @@ long time been our most requested feature. Finally, it's here!
**Developer support** **Developer support**
- Added optional background thread for debugging deadlocks. When the feature is - Added optional background thread for debugging deadlocks. When the feature is
enabled via the ``--debug-thread`` option or enabled via the ``mopidy --debug-thread`` option or
:attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump :attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump
the traceback for all running threads. the traceback for all running threads.
@ -2905,9 +2942,9 @@ resolved a bunch of related issues.
known setting, and suggests to the user what we think the setting should have known setting, and suggests to the user what we think the setting should have
been. been.
- Added :option:`--list-deps` option to the ``mopidy`` command that lists - Added ``mopidy --list-deps`` option that lists required and optional
required and optional dependencies, their current versions, and some other dependencies, their current versions, and some other information useful for
information useful for debugging. (Fixes: :issue:`74`) debugging. (Fixes: :issue:`74`)
- Added ``tools/debug-proxy.py`` to tee client requests to two backends and - Added ``tools/debug-proxy.py`` to tee client requests to two backends and
diff responses. Intended as a developer tool for checking for MPD protocol diff responses. Intended as a developer tool for checking for MPD protocol
@ -3187,12 +3224,12 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
- Command line usage: - Command line usage:
- Support passing options to GStreamer. See :option:`--help-gst` for a list - Support passing options to GStreamer. See ``mopidy --help-gst`` for a list
of available options. (Fixes: :issue:`95`) of available options. (Fixes: :issue:`95`)
- Improve :option:`--list-settings` output. (Fixes: :issue:`91`) - Improve ``mopidy --list-settings`` output. (Fixes: :issue:`91`)
- Added :option:`--interactive` for reading missing local settings from - Added ``mopidy --interactive`` for reading missing local settings from
``stdin``. (Fixes: :issue:`96`) ``stdin``. (Fixes: :issue:`96`)
- Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``, - Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``,
@ -3335,8 +3372,8 @@ loading from Mopidy 0.3.0 is still present.
- Settings: - Settings:
- Fix crash on ``--list-settings`` on clean installation. Thanks to Martins - Fix crash on ``mopidy --list-settings`` on clean installation. Thanks to
Grunskis for the bug report and patch. (Fixes: :issue:`63`) Martins Grunskis for the bug report and patch. (Fixes: :issue:`63`)
- Packaging: - Packaging:
@ -3564,11 +3601,11 @@ to Valentin David.
- Simplify the default log format, - Simplify the default log format,
:attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view: :attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view:
Less noise, more information. Less noise, more information.
- Rename the :option:`--dump` command line option to - Rename the ``mopidy --dump`` command line option to
:option:`--save-debug-log`. :option:`mopidy --save-debug-log`.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to - Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to
:attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose` :attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for
too. :option:`mopidy --verbose` too.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to - Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`. :attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
@ -3634,7 +3671,7 @@ fixing the OS X issues for a future release. You can track the progress at
- Exit early if not Python >= 2.6, < 3. - Exit early if not Python >= 2.6, < 3.
- Validate settings at startup and print useful error messages if the settings - Validate settings at startup and print useful error messages if the settings
has not been updated or anything is misspelled. has not been updated or anything is misspelled.
- Add command line option :option:`--list-settings` to print the currently - Add command line option ``mopidy --list-settings`` to print the currently
active settings. active settings.
- Include Sphinx scripts for building docs, pylintrc, tests and test data in - Include Sphinx scripts for building docs, pylintrc, tests and test data in
the packages created by ``setup.py`` for i.e. PyPI. the packages created by ``setup.py`` for i.e. PyPI.
@ -3816,7 +3853,7 @@ the established pace of at least a release per month.
- Improvements to MPD protocol handling, making Mopidy work much better with a - Improvements to MPD protocol handling, making Mopidy work much better with a
group of clients, including ncmpc, MPoD, and Theremin. group of clients, including ncmpc, MPoD, and Theremin.
- New command line flag :option:`--dump` for dumping debug log to ``dump.log`` - New command line flag ``mopidy --dump`` for dumping debug log to ``dump.log``
in the current directory. in the current directory.
- New setting :attr:`mopidy.settings.MIXER_ALSA_CONTROL` for forcing what ALSA - New setting :attr:`mopidy.settings.MIXER_ALSA_CONTROL` for forcing what ALSA
control :class:`mopidy.mixers.alsa.AlsaMixer` should use. control :class:`mopidy.mixers.alsa.AlsaMixer` should use.

View File

@ -40,7 +40,6 @@ MOCK_MODULES = [
'dbus.mainloop.glib', 'dbus.mainloop.glib',
'dbus.service', 'dbus.service',
'mopidy.internal.gi', 'mopidy.internal.gi',
'pykka',
] ]
for mod_name in MOCK_MODULES: for mod_name in MOCK_MODULES:
sys.modules[mod_name] = Mock() sys.modules[mod_name] = Mock()

View File

@ -97,15 +97,6 @@ Extension for playing music and audio from the `Internet Archive
<https://archive.org/>`_. <https://archive.org/>`_.
Mopidy-LeftAsRain
=================
https://github.com/naglis/mopidy-leftasrain
Extension for playing music from the `leftasrain.com
<http://leftasrain.com/>`_ music blog.
Mopidy-Local Mopidy-Local
============ ============

View File

@ -13,8 +13,7 @@ Creating releases
#. Update changelog and commit it. #. Update changelog and commit it.
#. Bump the version number in ``mopidy/__init__.py``. Remember to update the #. Bump the version number in ``mopidy/__init__.py``.
test case in ``tests/test_version.py``.
#. Merge the release branch (``develop`` in the example) into master:: #. Merge the release branch (``develop`` in the example) into master::
@ -63,93 +62,5 @@ Creating releases
#. Spread the word through the topic on #mopidy on IRC, @mopidy on Twitter, and #. Spread the word through the topic on #mopidy on IRC, @mopidy on Twitter, and
on the mailing list. on the mailing list.
#. Update the Debian package. #. Notify distribution packagers, including but not limited to: Debian, Arch
Linux, Homebrew.
Updating Debian packages
========================
This howto is not intended to learn you all the details, just to give someone
already familiar with Debian packaging an overview of how Mopidy's Debian
packages is maintained.
#. Install the basic packaging tools::
sudo apt-get install build-essential git-buildpackage
#. Create a Wheezy pbuilder env if running on Ubuntu and this the first time.
See :issue:`561` for details about why this is needed::
DIST=wheezy sudo git-pbuilder update --mirror=http://mirror.rackspace.com/debian/ --debootstrapopts --keyring=/usr/share/keyrings/debian-archive-keyring.gpg
#. Check out the ``debian`` branch of the repo::
git checkout -t origin/debian
git pull
#. Merge the latest release tag into the ``debian`` branch::
git merge v0.16.0
#. Update the ``debian/changelog`` with a "New upstream release" entry::
dch -v 0.16.0-0mopidy1
git add debian/changelog
git commit -m "debian: New upstream release"
#. Check if any dependencies in ``debian/control`` or similar needs updating.
#. Install any Build-Deps listed in ``debian/control``.
#. Build the package and fix any issues repeatedly until the build succeeds and
the Lintian check at the end of the build is satisfactory::
git buildpackage -uc -us
If you are using the pbuilder make sure this command is::
sudo git buildpackage -uc -us --git-ignore-new --git-pbuilder --git-dist=wheezy --git-no-pbuilder-autoconf
#. Install and test newly built package::
sudo debi
Again for pbuilder use::
sudo debi --debs-dir /var/cache/pbuilder/result/
#. If everything is OK, build the package a final time to tag the package
version::
git buildpackage -uc -us --git-tag
Pbuilder::
sudo git buildpackage -uc -us --git-ignore-new --git-pbuilder --git-dist=wheezy --git-no-pbuilder-autoconf --git-tag
#. Push the changes you've done to the ``debian`` branch and the new tag::
git push
git push --tags
#. If you're building for multiple architectures, checkout the ``debian``
branch on the other builders and run::
git buildpackage -uc -us
Modify as above to use the pbuilder as needed.
#. Copy files to the APT server. Make sure to select the correct part of the
repo, e.g. main, contrib, or non-free::
scp ../mopidy*_0.16* bonobo.mopidy.com:/srv/apt.mopidy.com/app/incoming/stable/main
#. Update the APT repo::
ssh bonobo.mopidy.com
/srv/apt.mopidy.com/app/update.sh
#. Test installation from apt.mopidy.com::
sudo apt-get update
sudo apt-get dist-upgrade

View File

@ -1,2 +1,4 @@
Sphinx >= 1.0 Sphinx >= 1.0
pygraphviz pygraphviz
Pykka >= 1.1
sphinx_rtd_theme

View File

@ -31,10 +31,3 @@ accelerate requests to all Mopidy services, including:
- https://dl.mopidy.com/pimusicbox/, which is used to distribute Pi Musicbox - https://dl.mopidy.com/pimusicbox/, which is used to distribute Pi Musicbox
images. images.
GlobalSign
==========
`GlobalSign <https://www.globalsign.com/>`_ provides Mopidy with a free SSL
certificate for mopidy.com, which we use to secure access to all our web sites.

View File

@ -14,4 +14,4 @@ if not (2, 7) <= sys.version_info < (3,):
warnings.filterwarnings('ignore', 'could not open display') warnings.filterwarnings('ignore', 'could not open display')
__version__ = '2.0.0' __version__ = '2.0.1'

View File

@ -5,7 +5,7 @@ import os
import signal import signal
import sys import sys
from mopidy.internal.gi import Gst # noqa: Import to initialize from mopidy.internal.gi import Gst # noqa: F401
try: try:
# Make GObject's mainloop the event loop for python-dbus # Make GObject's mainloop the event loop for python-dbus

View File

@ -22,7 +22,6 @@ logger = logging.getLogger(__name__)
gst_logger = logging.getLogger('mopidy.audio.gst') gst_logger = logging.getLogger('mopidy.audio.gst')
_GST_PLAY_FLAGS_AUDIO = 0x02 _GST_PLAY_FLAGS_AUDIO = 0x02
_GST_PLAY_FLAGS_SOFT_VOLUME = 0x10
_GST_STATE_MAPPING = { _GST_STATE_MAPPING = {
Gst.State.PLAYING: PlaybackState.PLAYING, Gst.State.PLAYING: PlaybackState.PLAYING,
@ -369,12 +368,16 @@ class _Handler(object):
# Emit any postponed tags that we got after about-to-finish. # Emit any postponed tags that we got after about-to-finish.
tags, self._audio._pending_tags = self._audio._pending_tags, None tags, self._audio._pending_tags = self._audio._pending_tags, None
self._audio._tags = tags self._audio._tags = tags or {}
if tags: if 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())
if self._audio._pending_metadata:
self._audio._playbin.send_event(self._audio._pending_metadata)
self._audio._pending_metadata = None
def on_segment(self, segment): def on_segment(self, segment):
gst_logger.debug( gst_logger.debug(
'Got SEGMENT pad event: ' 'Got SEGMENT pad event: '
@ -413,6 +416,7 @@ class Audio(pykka.ThreadingActor):
self._tags = {} self._tags = {}
self._pending_uri = None self._pending_uri = None
self._pending_tags = None self._pending_tags = None
self._pending_metadata = None
self._playbin = None self._playbin = None
self._outputs = None self._outputs = None
@ -451,8 +455,7 @@ class Audio(pykka.ThreadingActor):
def _setup_playbin(self): def _setup_playbin(self):
playbin = Gst.ElementFactory.make('playbin') playbin = Gst.ElementFactory.make('playbin')
playbin.set_property( playbin.set_property('flags', _GST_PLAY_FLAGS_AUDIO)
'flags', _GST_PLAY_FLAGS_AUDIO | _GST_PLAY_FLAGS_SOFT_VOLUME)
# 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
@ -489,15 +492,16 @@ class Audio(pykka.ThreadingActor):
def _setup_audio_sink(self): def _setup_audio_sink(self):
audio_sink = Gst.ElementFactory.make('bin', 'audio-sink') audio_sink = Gst.ElementFactory.make('bin', 'audio-sink')
queue = Gst.ElementFactory.make('queue')
volume = Gst.ElementFactory.make('volume')
# Queue element to buy us time between the about-to-finish event and # 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: See if settings should be set to minimize latency. Previous # TODO: See if settings should be set to minimize latency. Previous
# setting breaks appsrc, and settings before that broke on a few # setting breaks appsrc, and settings before that broke on a few
# systems. So leave the default to play it safe. # systems. So leave the default to play it safe.
queue = Gst.ElementFactory.make('queue')
if self._config['audio']['buffer_time'] > 0: if self._config['audio']['buffer_time'] > 0:
queue.set_property( queue.set_property(
'max-size-time', 'max-size-time',
@ -505,15 +509,13 @@ class Audio(pykka.ThreadingActor):
audio_sink.add(queue) audio_sink.add(queue)
audio_sink.add(self._outputs) audio_sink.add(self._outputs)
if self.mixer:
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)
if self.mixer:
self.mixer.setup(volume, self.actor_ref.proxy().mixer) self.mixer.setup(volume, self.actor_ref.proxy().mixer)
else:
queue.link(self._outputs)
ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink')) ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink'))
audio_sink.add_pad(ghost_pad) audio_sink.add_pad(ghost_pad)
@ -807,7 +809,9 @@ class Audio(pykka.ThreadingActor):
'Sending TAG event for track %r: %r', 'Sending TAG event for track %r: %r',
track.uri, taglist.to_string()) track.uri, taglist.to_string())
event = Gst.Event.new_tag(taglist) event = Gst.Event.new_tag(taglist)
# TODO: check if we get this back on our own bus? if self._pending_uri:
self._pending_metadata = event
else:
self._playbin.send_event(event) self._playbin.send_event(event)
def get_current_tags(self): def get_current_tags(self):

View File

@ -135,6 +135,17 @@ def _start_pipeline(pipeline):
pipeline.set_state(Gst.State.PLAYING) pipeline.set_state(Gst.State.PLAYING)
def _query_duration(pipeline):
success, duration = pipeline.query_duration(Gst.Format.TIME)
if not success:
duration = None # Make sure error case preserves None.
elif duration < 0:
duration = None # Stream without duration.
else:
duration = duration // Gst.MSECOND
return success, duration
def _query_seekable(pipeline): 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)
@ -187,13 +198,8 @@ def _process(pipeline, timeout_ms):
elif message.type == Gst.MessageType.EOS: elif message.type == Gst.MessageType.EOS:
return tags, mime, have_audio, duration return tags, mime, have_audio, duration
elif message.type == Gst.MessageType.ASYNC_DONE: elif message.type == Gst.MessageType.ASYNC_DONE:
success, duration = pipeline.query_duration(Gst.Format.TIME) success, duration = _query_duration(pipeline)
if success: if tags and success:
duration = duration // Gst.MSECOND
else:
duration = None
if tags and duration is not None:
return tags, mime, have_audio, duration return tags, mime, have_audio, duration
# Workaround for upstream bug which causes tags/duration to arrive # Workaround for upstream bug which causes tags/duration to arrive

View File

@ -44,10 +44,15 @@ gstreamer-GstTagList.html
value = taglist.get_value_index(tag, i) value = taglist.get_value_index(tag, i)
if isinstance(value, GLib.Date): if isinstance(value, GLib.Date):
try:
date = datetime.date( date = datetime.date(
value.get_year(), value.get_month(), value.get_day()) value.get_year(), value.get_month(), value.get_day())
result[tag].append(date.isoformat().decode('utf-8')) result[tag].append(date.isoformat().decode('utf-8'))
if isinstance(value, Gst.DateTime): except ValueError:
logger.debug(
'Ignoring dodgy date value: %d-%d-%d',
value.get_year(), value.get_month(), value.get_day())
elif isinstance(value, Gst.DateTime):
result[tag].append(value.to_iso8601_string().decode('utf-8')) result[tag].append(value.to_iso8601_string().decode('utf-8'))
elif isinstance(value, bytes): elif isinstance(value, bytes):
result[tag].append(value.decode('utf-8', 'replace')) result[tag].append(value.decode('utf-8', 'replace'))
@ -131,12 +136,11 @@ def convert_tags_to_track(tags):
return Track(**track_kwargs) return Track(**track_kwargs)
def _artists( def _artists(tags, artist_name, artist_id=None, artist_sortname=None):
tags, artist_name, artist_id=None, artist_sortname=None):
# Name missing, don't set artist # Name missing, don't set artist
if not tags.get(artist_name): if not tags.get(artist_name):
return None return None
# One artist name and either id or sortname, include all available fields # One artist name and either id or sortname, include all available fields
if len(tags[artist_name]) == 1 and \ if len(tags[artist_name]) == 1 and \
(artist_id in tags or artist_sortname in tags): (artist_id in tags or artist_sortname in tags):

View File

@ -9,12 +9,15 @@ import re
from mopidy import compat from mopidy import compat
from mopidy.compat import configparser from mopidy.compat import configparser
from mopidy.config import keyring from mopidy.config import keyring
from mopidy.config.schemas import * # noqa from mopidy.config.schemas import *
from mopidy.config.types import * # noqa from mopidy.config.types import *
from mopidy.internal import path, versioning from mopidy.internal import path, versioning
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# flake8: noqa:
# TODO: Update this to be flake8 compliant
_core_schema = ConfigSchema('core') _core_schema = ConfigSchema('core')
_core_schema['cache_dir'] = Path() _core_schema['cache_dir'] = Path()
_core_schema['config_dir'] = Path() _core_schema['config_dir'] = Path()

View File

@ -239,8 +239,8 @@ class PlaybackController(object):
self._seek(self._pending_position) self._seek(self._pending_position)
def _on_position_changed(self, position): def _on_position_changed(self, position):
if self._pending_position == position: if self._pending_position is not None:
self._trigger_seeked(position) self._trigger_seeked(self._pending_position)
self._pending_position = None self._pending_position = None
if self._start_paused: if self._start_paused:
self._start_paused = False self._start_paused = False
@ -263,6 +263,12 @@ class PlaybackController(object):
if self._state == PlaybackState.STOPPED: if self._state == PlaybackState.STOPPED:
return return
# Unless overridden by other calls (e.g. next / previous / stop) this
# will be the last position recorded until the track gets reassigned.
# TODO: Check if case when track.length isn't populated needs to be
# handled.
self._last_position = self._current_tl_track.track.length
pending = self.core.tracklist.eot_track(self._current_tl_track) pending = self.core.tracklist.eot_track(self._current_tl_track)
# avoid endless loop if 'repeat' is 'true' and no track is playable # avoid endless loop if 'repeat' is 'true' and no track is playable
# * 2 -> second run to get all playable track in a shuffled playlist # * 2 -> second run to get all playable track in a shuffled playlist
@ -406,6 +412,10 @@ class PlaybackController(object):
if not backend: if not backend:
return False return False
# This must happen before prepare_change gets called, otherwise the
# backend flushes the information of the track.
self._last_position = self.get_time_position()
# TODO: Wrap backend call in error handling. # TODO: Wrap backend call in error handling.
backend.playback.prepare_change() backend.playback.prepare_change()

View File

@ -192,9 +192,9 @@ class Track(ValidatedImmutableObject):
:param album: track album :param album: track album
:type album: :class:`Album` :type album: :class:`Album`
:param composers: track composers :param composers: track composers
:type composers: string :type composers: list of :class:`Artist`
:param performers: track performers :param performers: track performers
:type performers: string :type performers: list of :class:`Artist`
:param genre: track genre :param genre: track genre
:type genre: string :type genre: string
:param track_no: track number in album :param track_no: track number in album

View File

@ -88,14 +88,17 @@ class Date(String):
class Identifier(String): class Identifier(String):
""" """
:class:`Field` for storing ASCII values such as GUIDs or other identifiers. :class:`Field` for storing values such as GUIDs or other identifiers.
Values will be interned. Values will be interned.
:param default: default value for field :param default: default value for field
""" """
def validate(self, value): def validate(self, value):
return compat.intern(str(super(Identifier, self).validate(value))) value = super(Identifier, self).validate(value)
if isinstance(value, compat.text_type):
value = value.encode('utf-8')
return compat.intern(value)
class URI(Identifier): class URI(Identifier):

View File

@ -325,7 +325,7 @@ def replay_gain_status(context):
Prints replay gain options. Currently, only the variable Prints replay gain options. Currently, only the variable
``replay_gain_mode`` is returned. ``replay_gain_mode`` is returned.
""" """
return 'off' # TODO return 'replay_gain_mode: off' # TODO
@protocol.commands.add('seek', songpos=protocol.UINT, seconds=protocol.UINT) @protocol.commands.add('seek', songpos=protocol.UINT, seconds=protocol.UINT)

View File

@ -173,6 +173,7 @@ def status(context):
decimal places for millisecond precision. decimal places for millisecond precision.
""" """
tl_track = context.core.playback.get_current_tl_track() tl_track = context.core.playback.get_current_tl_track()
next_tlid = context.core.tracklist.get_next_tlid()
futures = { futures = {
'tracklist.length': context.core.tracklist.get_length(), 'tracklist.length': context.core.tracklist.get_length(),
@ -185,6 +186,9 @@ def status(context):
'playback.state': context.core.playback.get_state(), 'playback.state': context.core.playback.get_state(),
'playback.current_tl_track': tl_track, 'playback.current_tl_track': tl_track,
'tracklist.index': context.core.tracklist.index(tl_track.get()), 'tracklist.index': context.core.tracklist.index(tl_track.get()),
'tracklist.next_tlid': next_tlid,
'tracklist.next_index': context.core.tracklist.index(
tlid=next_tlid.get()),
'playback.time_position': context.core.playback.get_time_position(), 'playback.time_position': context.core.playback.get_time_position(),
} }
pykka.get_all(futures.values()) pykka.get_all(futures.values())
@ -199,10 +203,12 @@ def status(context):
('xfade', _status_xfade(futures)), ('xfade', _status_xfade(futures)),
('state', _status_state(futures)), ('state', _status_state(futures)),
] ]
# TODO: add nextsong and nextsongid
if futures['playback.current_tl_track'].get() is not None: if futures['playback.current_tl_track'].get() is not None:
result.append(('song', _status_songpos(futures))) result.append(('song', _status_songpos(futures)))
result.append(('songid', _status_songid(futures))) result.append(('songid', _status_songid(futures)))
if futures['tracklist.next_tlid'].get() is not None:
result.append(('nextsong', _status_nextsongpos(futures)))
result.append(('nextsongid', _status_nextsongid(futures)))
if futures['playback.state'].get() in ( if futures['playback.state'].get() in (
PlaybackState.PLAYING, PlaybackState.PAUSED): PlaybackState.PLAYING, PlaybackState.PAUSED):
result.append(('time', _status_time(futures))) result.append(('time', _status_time(futures)))
@ -259,6 +265,14 @@ def _status_songpos(futures):
return futures['tracklist.index'].get() return futures['tracklist.index'].get()
def _status_nextsongid(futures):
return futures['tracklist.next_tlid'].get()
def _status_nextsongpos(futures):
return futures['tracklist.next_index'].get()
def _status_state(futures): def _status_state(futures):
state = futures['playback.state'].get() state = futures['playback.state'].get()
if state == PlaybackState.PLAYING: if state == PlaybackState.PLAYING:

View File

@ -68,7 +68,7 @@ class StreamLibraryProvider(backend.LibraryProvider):
track = tags.convert_tags_to_track(scan_result.tags).replace( track = tags.convert_tags_to_track(scan_result.tags).replace(
uri=uri, length=scan_result.duration) uri=uri, length=scan_result.duration)
else: else:
logger.warning('Problem looking up %s: %s', uri) logger.warning('Problem looking up %s', uri)
track = Track(uri=uri) track = Track(uri=uri)
return [track] return [track]
@ -142,7 +142,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
uri, timeout) uri, timeout)
return None, None return None, None
content = http.download( content = http.download(
requests_session, uri, timeout=download_timeout) requests_session, uri, timeout=download_timeout / 1000)
if content is None: if content is None:
logger.info( logger.info(

View File

@ -44,6 +44,14 @@ class TestConvertTaglist(object):
assert isinstance(result[Gst.TAG_DATE][0], compat.text_type) assert isinstance(result[Gst.TAG_DATE][0], compat.text_type)
assert result[Gst.TAG_DATE][0] == '2014-01-07' assert result[Gst.TAG_DATE][0] == '2014-01-07'
def test_date_tag_bad_value(self):
date = GLib.Date.new_dmy(7, 1, 10000)
taglist = self.make_taglist(Gst.TAG_DATE, [date])
result = tags.convert_taglist(taglist)
assert len(result[Gst.TAG_DATE]) == 0
def test_date_time_tag(self): def test_date_time_tag(self):
taglist = self.make_taglist(Gst.TAG_DATE_TIME, [ taglist = self.make_taglist(Gst.TAG_DATE_TIME, [
Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12') Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12')

View File

@ -735,6 +735,7 @@ class EventEmissionTest(BaseTest):
self.core.playback.play(tl_tracks[0]) self.core.playback.play(tl_tracks[0])
self.trigger_about_to_finish(replay_until='stream_changed') self.trigger_about_to_finish(replay_until='stream_changed')
self.replay_events()
listener_mock.reset_mock() listener_mock.reset_mock()
self.core.playback.seek(1000) self.core.playback.seek(1000)

View File

@ -1,8 +1,11 @@
# encoding: utf-8
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import unittest import unittest
from mopidy.models.fields import * # noqa: F403 from mopidy.models.fields import (Boolean, Collection, Field, Identifier,
Integer, String)
def create_instance(field): def create_instance(field):
@ -126,6 +129,42 @@ class StringTest(unittest.TestCase):
self.assertEqual('', instance.attr) self.assertEqual('', instance.attr)
class IdentifierTest(unittest.TestCase):
def test_default_handling(self):
instance = create_instance(Identifier(default='abc'))
self.assertEqual('abc', instance.attr)
def test_native_str_allowed(self):
instance = create_instance(Identifier())
instance.attr = str('abc')
self.assertEqual('abc', instance.attr)
def test_bytes_allowed(self):
instance = create_instance(Identifier())
instance.attr = b'abc'
self.assertEqual(b'abc', instance.attr)
def test_unicode_allowed(self):
instance = create_instance(Identifier())
instance.attr = u'abc'
self.assertEqual(u'abc', instance.attr)
def test_unicode_with_nonascii_allowed(self):
instance = create_instance(Identifier())
instance.attr = u'æøå'
self.assertEqual(u'æøå'.encode('utf-8'), instance.attr)
def test_other_disallowed(self):
instance = create_instance(Identifier())
with self.assertRaises(TypeError):
instance.attr = 1234
def test_empty_string(self):
instance = create_instance(Identifier())
instance.attr = ''
self.assertEqual('', instance.attr)
class IntegerTest(unittest.TestCase): class IntegerTest(unittest.TestCase):
def test_default_handling(self): def test_default_handling(self):
instance = create_instance(Integer(default=1234)) instance = create_instance(Integer(default=1234))

View File

@ -115,7 +115,7 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_replay_gain_status_default(self): def test_replay_gain_status_default(self):
self.send_request('replay_gain_status') self.send_request('replay_gain_status')
self.assertInResponse('OK') self.assertInResponse('OK')
self.assertInResponse('off') self.assertInResponse('replay_gain_mode: off')
def test_mixrampdb(self): def test_mixrampdb(self):
self.send_request('mixrampdb "10"') self.send_request('mixrampdb "10"')

View File

@ -48,9 +48,9 @@ class StatusHandlerTest(unittest.TestCase):
def tearDown(self): # noqa: N802 def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all() pykka.ActorRegistry.stop_all()
def set_tracklist(self, track): def set_tracklist(self, tracks):
self.backend.library.dummy_library = [track] self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[track.uri]).get() self.core.tracklist.add(uris=[track.uri for track in tracks]).get()
def test_stats_method(self): def test_stats_method(self):
result = status.stats(self.context) result = status.stats(self.context)
@ -154,22 +154,35 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['state'], 'pause') self.assertEqual(result['state'], 'pause')
def test_status_method_when_playlist_loaded_contains_song(self): def test_status_method_when_playlist_loaded_contains_song(self):
self.set_tracklist(Track(uri='dummy:/a')) self.set_tracklist([Track(uri='dummy:/a')])
self.core.playback.play().get() self.core.playback.play().get()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assertIn('song', result) self.assertIn('song', result)
self.assertGreaterEqual(int(result['song']), 0) self.assertGreaterEqual(int(result['song']), 0)
def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self): def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self):
self.set_tracklist(Track(uri='dummy:/a')) self.set_tracklist([Track(uri='dummy:/a')])
self.core.playback.play().get() self.core.playback.play().get()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assertIn('songid', result) self.assertIn('songid', result)
self.assertEqual(int(result['songid']), 1) self.assertEqual(int(result['songid']), 1)
def test_status_method_when_playlist_loaded_contains_nextsong(self):
self.set_tracklist([Track(uri='dummy:/a'), Track(uri='dummy:/b')])
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('nextsong', result)
self.assertGreaterEqual(int(result['nextsong']), 0)
def test_status_method_when_playlist_loaded_contains_nextsongid(self):
self.set_tracklist([Track(uri='dummy:/a'), Track(uri='dummy:/b')])
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('nextsongid', result)
self.assertEqual(int(result['nextsongid']), 2)
def test_status_method_when_playing_contains_time_with_no_length(self): def test_status_method_when_playing_contains_time_with_no_length(self):
self.set_tracklist(Track(uri='dummy:/a', length=None)) self.set_tracklist([Track(uri='dummy:/a', length=None)])
self.core.playback.play().get() self.core.playback.play().get()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assertIn('time', result) self.assertIn('time', result)
@ -179,7 +192,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertLessEqual(position, total) self.assertLessEqual(position, total)
def test_status_method_when_playing_contains_time_with_length(self): def test_status_method_when_playing_contains_time_with_length(self):
self.set_tracklist(Track(uri='dummy:/a', length=10000)) self.set_tracklist([Track(uri='dummy:/a', length=10000)])
self.core.playback.play() self.core.playback.play()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assertIn('time', result) self.assertIn('time', result)
@ -189,7 +202,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertLessEqual(position, total) self.assertLessEqual(position, total)
def test_status_method_when_playing_contains_elapsed(self): def test_status_method_when_playing_contains_elapsed(self):
self.set_tracklist(Track(uri='dummy:/a', length=60000)) self.set_tracklist([Track(uri='dummy:/a', length=60000)])
self.core.playback.play().get() self.core.playback.play().get()
self.core.playback.pause() self.core.playback.pause()
self.core.playback.seek(59123) self.core.playback.seek(59123)
@ -198,7 +211,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['elapsed'], '59.123') self.assertEqual(result['elapsed'], '59.123')
def test_status_method_when_starting_playing_contains_elapsed_zero(self): def test_status_method_when_starting_playing_contains_elapsed_zero(self):
self.set_tracklist(Track(uri='dummy:/a', length=10000)) self.set_tracklist([Track(uri='dummy:/a', length=10000)])
self.core.playback.play().get() self.core.playback.play().get()
self.core.playback.pause() self.core.playback.pause()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
@ -206,7 +219,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['elapsed'], '0.000') self.assertEqual(result['elapsed'], '0.000')
def test_status_method_when_playing_contains_bitrate(self): def test_status_method_when_playing_contains_bitrate(self):
self.set_tracklist(Track(uri='dummy:/a', bitrate=3200)) self.set_tracklist([Track(uri='dummy:/a', bitrate=3200)])
self.core.playback.play().get() self.core.playback.play().get()
result = dict(status.status(self.context)) result = dict(status.status(self.context))
self.assertIn('bitrate', result) self.assertIn('bitrate', result)

View File

@ -37,7 +37,8 @@ commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
[testenv:flake8] [testenv:flake8]
deps = deps =
flake8 flake8
flake8-import-order # TODO: Re-enable once https://github.com/PyCQA/flake8-import-order/issues/79 is released.
# flake8-import-order
pep8-naming pep8-naming
commands = flake8 --show-source --statistics mopidy tests commands = flake8 --show-source --statistics mopidy tests