diff --git a/README.rst b/README.rst
index 88072eb5..6720e9e8 100644
--- a/README.rst
+++ b/README.rst
@@ -61,10 +61,6 @@ To get started with Mopidy, check out
:target: https://pypi.python.org/pypi/Mopidy/
:alt: Latest PyPI version
-.. image:: https://img.shields.io/pypi/dm/Mopidy.svg?style=flat
- :target: https://pypi.python.org/pypi/Mopidy/
- :alt: Number of PyPI downloads
-
.. image:: https://img.shields.io/travis/mopidy/mopidy/develop.svg?style=flat
:target: https://travis-ci.org/mopidy/mopidy
:alt: Travis CI build status
diff --git a/docs/changelog.rst b/docs/changelog.rst
index b048e726..5573da6d 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -13,8 +13,14 @@ Feature release.
- Core: Mopidy restores its last state when started. Can be enabled by setting
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.
@@ -29,16 +35,48 @@ Bug fix release.
- Audio: Update scan logic to workaround GStreamer issue where tags and
duration might only be available after we start playing.
- (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474` and :issue:`1480`, PR:
+ (Fixes: :issue:`935`, :issue:`1453`, :issue:`1474`, :issue:`1480`, PR:
:issue:`1487`)
+- Audio: Better handling of seek when position does not match the expected
+ 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
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.
(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)
===================
@@ -378,7 +416,7 @@ Bug fix release.
proceed startup. (Fixes: :issue:`1248`, PR: :issue:`1254`)
- Stream: Fix bug in new playlist parser. A non-ASCII char in an urilist
- comment would cause a crash while parsing due to comparision of a non-ASCII
+ comment would cause a crash while parsing due to comparison of a non-ASCII
bytestring with a Unicode string. (Fixes: :issue:`1265`)
- File: Adjust log levels when failing to expand ``$XDG_MUSIC_DIR`` into a real
@@ -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
options.
-- :option:`mopidy --show-config` will now take into consideration any
+- ``mopidy --show-config`` will now take into consideration any
:option:`mopidy --option` arguments appearing later on the command line. This
helps you see the effective configuration for runs with the same
- :option:`mopidy --options` arguments.
+ ``mopidy --options`` arguments.
**Audio**
@@ -2158,7 +2196,7 @@ v0.14.1 (2013-04-28)
====================
This release addresses an issue in v0.14.0 where the new
-:option:`mopidy-convert-config` tool and the new :option:`mopidy --option`
+``mopidy-convert-config`` tool and the new :option:`mopidy --option`
command line option was broken because some string operations inadvertently
converted some byte strings to unicode.
@@ -2188,7 +2226,7 @@ one new.
As part of this change we have cleaned up the naming of our config values.
- To ease migration we've made a tool named :option:`mopidy-convert-config` for
+ To ease migration we've made a tool named ``mopidy-convert-config`` for
automatically converting the old ``settings.py`` to a new ``mopidy.conf``
file. This tool takes care of all the renamed config values as well. See
``mopidy-convert-config`` for details on how to use it.
@@ -2221,11 +2259,11 @@ one new.
**Command line options**
-- The command option :option:`mopidy --list-settings` is now named
- :option:`mopidy --show-config`.
+- The command option ``mopidy --list-settings`` is now named
+ ``mopidy --show-config``.
-- The command option :option:`mopidy --list-deps` is now named
- :option:`mopidy --show-deps`.
+- The command option ``mopidy --list-deps`` is now named
+ ``mopidy --show-deps``.
- What configuration files to use can now be specified through the command
option :option:`mopidy --config`, multiple files can be specified using colon
@@ -2235,8 +2273,8 @@ one new.
:option:`mopidy --option`. For example: ``mopidy --option
spotify/enabled=false``.
-- The GStreamer command line options, :option:`mopidy --gst-*` and
- :option:`mopidy --help-gst` are no longer supported. To set GStreamer debug
+- The GStreamer command line options, ``mopidy --gst-*`` and
+ ``mopidy --help-gst`` are no longer supported. To set GStreamer debug
flags, you can use environment variables such as :envvar:`GST_DEBUG`. Refer
to GStreamer's documentation for details.
@@ -2285,7 +2323,7 @@ already have.
**Core**
- Removed the :attr:`mopidy.settings.DEBUG_THREAD` setting and the
- :option:`--debug-thread` command line option. Sending SIGUSR1 to
+ ``mopidy --debug-thread`` command line option. Sending SIGUSR1 to
the Mopidy process will now always make it log tracebacks for all alive
threads.
@@ -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
range 1-9999.
-- Make ``mopidy-scan`` accept :option:`-q`/:option:`--quiet` and
- :option:`-v`/:option:`--verbose` options to control the amount of logging
- output when scanning.
+- Make ``mopidy-scan`` accept ``-q``/``--quiet`` and ``-v``/``--verbose``
+ options to control the amount of logging output when scanning.
- The scanner can now handle files with other encodings than UTF-8. Rebuild
your tag cache with ``mopidy-scan`` to include tracks that may have been
@@ -2693,7 +2730,7 @@ long time been our most requested feature. Finally, it's here!
**Developer support**
- Added optional background thread for debugging deadlocks. When the feature is
- enabled via the ``--debug-thread`` option or
+ enabled via the ``mopidy --debug-thread`` option or
:attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump
the traceback for all running threads.
@@ -2905,9 +2942,9 @@ resolved a bunch of related issues.
known setting, and suggests to the user what we think the setting should have
been.
-- Added :option:`--list-deps` option to the ``mopidy`` command that lists
- required and optional dependencies, their current versions, and some other
- information useful for debugging. (Fixes: :issue:`74`)
+- Added ``mopidy --list-deps`` option that lists required and optional
+ dependencies, their current versions, and some other information useful for
+ debugging. (Fixes: :issue:`74`)
- Added ``tools/debug-proxy.py`` to tee client requests to two backends and
diff responses. Intended as a developer tool for checking for MPD protocol
@@ -3187,12 +3224,12 @@ Please note that 0.5.0 requires some updated dependencies, as listed under
- Command line usage:
- - Support passing options to GStreamer. See :option:`--help-gst` for a list
+ - Support passing options to GStreamer. See ``mopidy --help-gst`` for a list
of available options. (Fixes: :issue:`95`)
- - Improve :option:`--list-settings` output. (Fixes: :issue:`91`)
+ - Improve ``mopidy --list-settings`` output. (Fixes: :issue:`91`)
- - Added :option:`--interactive` for reading missing local settings from
+ - Added ``mopidy --interactive`` for reading missing local settings from
``stdin``. (Fixes: :issue:`96`)
- Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``,
@@ -3335,8 +3372,8 @@ loading from Mopidy 0.3.0 is still present.
- Settings:
- - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins
- Grunskis for the bug report and patch. (Fixes: :issue:`63`)
+ - Fix crash on ``mopidy --list-settings`` on clean installation. Thanks to
+ Martins Grunskis for the bug report and patch. (Fixes: :issue:`63`)
- Packaging:
@@ -3564,11 +3601,11 @@ to Valentin David.
- Simplify the default log format,
:attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view:
Less noise, more information.
- - Rename the :option:`--dump` command line option to
- :option:`--save-debug-log`.
+ - Rename the ``mopidy --dump`` command line option to
+ :option:`mopidy --save-debug-log`.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to
- :attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose`
- too.
+ :attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for
+ :option:`mopidy --verbose` too.
- Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to
:attr:`mopidy.settings.DEBUG_LOG_FILENAME`.
@@ -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.
- Validate settings at startup and print useful error messages if the settings
has not been updated or anything is misspelled.
-- Add command line option :option:`--list-settings` to print the currently
+- Add command line option ``mopidy --list-settings`` to print the currently
active settings.
- Include Sphinx scripts for building docs, pylintrc, tests and test data in
the packages created by ``setup.py`` for i.e. PyPI.
@@ -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
group of clients, including ncmpc, MPoD, and Theremin.
-- New command line flag :option:`--dump` for dumping debug log to ``dump.log``
+- New command line flag ``mopidy --dump`` for dumping debug log to ``dump.log``
in the current directory.
- New setting :attr:`mopidy.settings.MIXER_ALSA_CONTROL` for forcing what ALSA
control :class:`mopidy.mixers.alsa.AlsaMixer` should use.
diff --git a/docs/conf.py b/docs/conf.py
index 208822a2..cb04a671 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -40,7 +40,6 @@ MOCK_MODULES = [
'dbus.mainloop.glib',
'dbus.service',
'mopidy.internal.gi',
- 'pykka',
]
for mod_name in MOCK_MODULES:
sys.modules[mod_name] = Mock()
diff --git a/docs/ext/backends.rst b/docs/ext/backends.rst
index 2349006b..9e5f4a9c 100644
--- a/docs/ext/backends.rst
+++ b/docs/ext/backends.rst
@@ -97,15 +97,6 @@ Extension for playing music and audio from the `Internet Archive
`_.
-Mopidy-LeftAsRain
-=================
-
-https://github.com/naglis/mopidy-leftasrain
-
-Extension for playing music from the `leftasrain.com
-`_ music blog.
-
-
Mopidy-Local
============
diff --git a/docs/releasing.rst b/docs/releasing.rst
index 8d489146..e7ef251c 100644
--- a/docs/releasing.rst
+++ b/docs/releasing.rst
@@ -13,8 +13,7 @@ Creating releases
#. Update changelog and commit it.
-#. Bump the version number in ``mopidy/__init__.py``. Remember to update the
- test case in ``tests/test_version.py``.
+#. Bump the version number in ``mopidy/__init__.py``.
#. Merge the release branch (``develop`` in the example) into master::
@@ -63,93 +62,5 @@ Creating releases
#. Spread the word through the topic on #mopidy on IRC, @mopidy on Twitter, and
on the mailing list.
-#. Update the Debian package.
-
-
-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
+#. Notify distribution packagers, including but not limited to: Debian, Arch
+ Linux, Homebrew.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index c75793d9..f0cc5e6c 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,2 +1,4 @@
Sphinx >= 1.0
pygraphviz
+Pykka >= 1.1
+sphinx_rtd_theme
diff --git a/docs/sponsors.rst b/docs/sponsors.rst
index 2d8b7f4e..2528247b 100644
--- a/docs/sponsors.rst
+++ b/docs/sponsors.rst
@@ -31,10 +31,3 @@ accelerate requests to all Mopidy services, including:
- https://dl.mopidy.com/pimusicbox/, which is used to distribute Pi Musicbox
images.
-
-
-GlobalSign
-==========
-
-`GlobalSign `_ provides Mopidy with a free SSL
-certificate for mopidy.com, which we use to secure access to all our web sites.
diff --git a/mopidy/__init__.py b/mopidy/__init__.py
index 4a6370e8..184f5991 100644
--- a/mopidy/__init__.py
+++ b/mopidy/__init__.py
@@ -14,4 +14,4 @@ if not (2, 7) <= sys.version_info < (3,):
warnings.filterwarnings('ignore', 'could not open display')
-__version__ = '2.0.0'
+__version__ = '2.0.1'
diff --git a/mopidy/__main__.py b/mopidy/__main__.py
index 86a0c19c..7963900e 100644
--- a/mopidy/__main__.py
+++ b/mopidy/__main__.py
@@ -5,7 +5,7 @@ import os
import signal
import sys
-from mopidy.internal.gi import Gst # noqa: Import to initialize
+from mopidy.internal.gi import Gst # noqa: F401
try:
# Make GObject's mainloop the event loop for python-dbus
diff --git a/mopidy/audio/actor.py b/mopidy/audio/actor.py
index 267b228d..6020bc1b 100644
--- a/mopidy/audio/actor.py
+++ b/mopidy/audio/actor.py
@@ -22,7 +22,6 @@ logger = logging.getLogger(__name__)
gst_logger = logging.getLogger('mopidy.audio.gst')
_GST_PLAY_FLAGS_AUDIO = 0x02
-_GST_PLAY_FLAGS_SOFT_VOLUME = 0x10
_GST_STATE_MAPPING = {
Gst.State.PLAYING: PlaybackState.PLAYING,
@@ -369,12 +368,16 @@ class _Handler(object):
# Emit any postponed tags that we got after about-to-finish.
tags, self._audio._pending_tags = self._audio._pending_tags, None
- self._audio._tags = tags
+ self._audio._tags = tags or {}
if tags:
logger.debug('Audio event: tags_changed(tags=%r)', tags.keys())
AudioListener.send('tags_changed', tags=tags.keys())
+ if self._audio._pending_metadata:
+ self._audio._playbin.send_event(self._audio._pending_metadata)
+ self._audio._pending_metadata = None
+
def on_segment(self, segment):
gst_logger.debug(
'Got SEGMENT pad event: '
@@ -413,6 +416,7 @@ class Audio(pykka.ThreadingActor):
self._tags = {}
self._pending_uri = None
self._pending_tags = None
+ self._pending_metadata = None
self._playbin = None
self._outputs = None
@@ -451,8 +455,7 @@ class Audio(pykka.ThreadingActor):
def _setup_playbin(self):
playbin = Gst.ElementFactory.make('playbin')
- playbin.set_property(
- 'flags', _GST_PLAY_FLAGS_AUDIO | _GST_PLAY_FLAGS_SOFT_VOLUME)
+ playbin.set_property('flags', _GST_PLAY_FLAGS_AUDIO)
# TODO: turn into config values...
playbin.set_property('buffer-size', 5 << 20) # 5MB
@@ -489,15 +492,16 @@ class Audio(pykka.ThreadingActor):
def _setup_audio_sink(self):
audio_sink = Gst.ElementFactory.make('bin', 'audio-sink')
+ queue = Gst.ElementFactory.make('queue')
+ volume = Gst.ElementFactory.make('volume')
# Queue element to buy us time between the about-to-finish event and
# the actual switch, i.e. about to switch can block for longer thanks
# to this queue.
+
# TODO: See if settings should be set to minimize latency. Previous
# setting breaks appsrc, and settings before that broke on a few
# systems. So leave the default to play it safe.
- queue = Gst.ElementFactory.make('queue')
-
if self._config['audio']['buffer_time'] > 0:
queue.set_property(
'max-size-time',
@@ -505,15 +509,13 @@ class Audio(pykka.ThreadingActor):
audio_sink.add(queue)
audio_sink.add(self._outputs)
+ audio_sink.add(volume)
+
+ queue.link(volume)
+ volume.link(self._outputs)
if self.mixer:
- volume = Gst.ElementFactory.make('volume')
- audio_sink.add(volume)
- queue.link(volume)
- volume.link(self._outputs)
self.mixer.setup(volume, self.actor_ref.proxy().mixer)
- else:
- queue.link(self._outputs)
ghost_pad = Gst.GhostPad.new('sink', queue.get_static_pad('sink'))
audio_sink.add_pad(ghost_pad)
@@ -807,8 +809,10 @@ class Audio(pykka.ThreadingActor):
'Sending TAG event for track %r: %r',
track.uri, taglist.to_string())
event = Gst.Event.new_tag(taglist)
- # TODO: check if we get this back on our own bus?
- self._playbin.send_event(event)
+ if self._pending_uri:
+ self._pending_metadata = event
+ else:
+ self._playbin.send_event(event)
def get_current_tags(self):
"""
diff --git a/mopidy/audio/scan.py b/mopidy/audio/scan.py
index 27888638..f99c4489 100644
--- a/mopidy/audio/scan.py
+++ b/mopidy/audio/scan.py
@@ -135,6 +135,17 @@ def _start_pipeline(pipeline):
pipeline.set_state(Gst.State.PLAYING)
+def _query_duration(pipeline):
+ success, duration = pipeline.query_duration(Gst.Format.TIME)
+ if not success:
+ duration = None # Make sure error case preserves None.
+ elif duration < 0:
+ duration = None # Stream without duration.
+ else:
+ duration = duration // Gst.MSECOND
+ return success, duration
+
+
def _query_seekable(pipeline):
query = Gst.Query.new_seeking(Gst.Format.TIME)
pipeline.query(query)
@@ -187,13 +198,8 @@ def _process(pipeline, timeout_ms):
elif message.type == Gst.MessageType.EOS:
return tags, mime, have_audio, duration
elif message.type == Gst.MessageType.ASYNC_DONE:
- success, duration = pipeline.query_duration(Gst.Format.TIME)
- if success:
- duration = duration // Gst.MSECOND
- else:
- duration = None
-
- if tags and duration is not None:
+ success, duration = _query_duration(pipeline)
+ if tags and success:
return tags, mime, have_audio, duration
# Workaround for upstream bug which causes tags/duration to arrive
diff --git a/mopidy/audio/tags.py b/mopidy/audio/tags.py
index 5ae86468..e4d86dc7 100644
--- a/mopidy/audio/tags.py
+++ b/mopidy/audio/tags.py
@@ -44,10 +44,15 @@ gstreamer-GstTagList.html
value = taglist.get_value_index(tag, i)
if isinstance(value, GLib.Date):
- date = datetime.date(
- value.get_year(), value.get_month(), value.get_day())
- result[tag].append(date.isoformat().decode('utf-8'))
- if isinstance(value, Gst.DateTime):
+ try:
+ date = datetime.date(
+ value.get_year(), value.get_month(), value.get_day())
+ result[tag].append(date.isoformat().decode('utf-8'))
+ except ValueError:
+ logger.debug(
+ 'Ignoring dodgy date value: %d-%d-%d',
+ value.get_year(), value.get_month(), value.get_day())
+ elif isinstance(value, Gst.DateTime):
result[tag].append(value.to_iso8601_string().decode('utf-8'))
elif isinstance(value, bytes):
result[tag].append(value.decode('utf-8', 'replace'))
@@ -131,12 +136,11 @@ def convert_tags_to_track(tags):
return Track(**track_kwargs)
-def _artists(
- tags, artist_name, artist_id=None, artist_sortname=None):
-
+def _artists(tags, artist_name, artist_id=None, artist_sortname=None):
# Name missing, don't set artist
if not tags.get(artist_name):
return None
+
# One artist name and either id or sortname, include all available fields
if len(tags[artist_name]) == 1 and \
(artist_id in tags or artist_sortname in tags):
diff --git a/mopidy/config/__init__.py b/mopidy/config/__init__.py
index 7827e52d..2743625e 100644
--- a/mopidy/config/__init__.py
+++ b/mopidy/config/__init__.py
@@ -9,12 +9,15 @@ import re
from mopidy import compat
from mopidy.compat import configparser
from mopidy.config import keyring
-from mopidy.config.schemas import * # noqa
-from mopidy.config.types import * # noqa
+from mopidy.config.schemas import *
+from mopidy.config.types import *
from mopidy.internal import path, versioning
logger = logging.getLogger(__name__)
+# flake8: noqa:
+# TODO: Update this to be flake8 compliant
+
_core_schema = ConfigSchema('core')
_core_schema['cache_dir'] = Path()
_core_schema['config_dir'] = Path()
diff --git a/mopidy/core/playback.py b/mopidy/core/playback.py
index 1c809656..6abcc837 100644
--- a/mopidy/core/playback.py
+++ b/mopidy/core/playback.py
@@ -239,8 +239,8 @@ class PlaybackController(object):
self._seek(self._pending_position)
def _on_position_changed(self, position):
- if self._pending_position == position:
- self._trigger_seeked(position)
+ if self._pending_position is not None:
+ self._trigger_seeked(self._pending_position)
self._pending_position = None
if self._start_paused:
self._start_paused = False
@@ -263,6 +263,12 @@ class PlaybackController(object):
if self._state == PlaybackState.STOPPED:
return
+ # Unless overridden by other calls (e.g. next / previous / stop) this
+ # will be the last position recorded until the track gets reassigned.
+ # TODO: Check if case when track.length isn't populated needs to be
+ # handled.
+ self._last_position = self._current_tl_track.track.length
+
pending = self.core.tracklist.eot_track(self._current_tl_track)
# avoid endless loop if 'repeat' is 'true' and no track is playable
# * 2 -> second run to get all playable track in a shuffled playlist
@@ -406,6 +412,10 @@ class PlaybackController(object):
if not backend:
return False
+ # This must happen before prepare_change gets called, otherwise the
+ # backend flushes the information of the track.
+ self._last_position = self.get_time_position()
+
# TODO: Wrap backend call in error handling.
backend.playback.prepare_change()
diff --git a/mopidy/models/__init__.py b/mopidy/models/__init__.py
index f477a323..368739e0 100644
--- a/mopidy/models/__init__.py
+++ b/mopidy/models/__init__.py
@@ -192,9 +192,9 @@ class Track(ValidatedImmutableObject):
:param album: track album
:type album: :class:`Album`
:param composers: track composers
- :type composers: string
+ :type composers: list of :class:`Artist`
:param performers: track performers
- :type performers: string
+ :type performers: list of :class:`Artist`
:param genre: track genre
:type genre: string
:param track_no: track number in album
diff --git a/mopidy/models/fields.py b/mopidy/models/fields.py
index 178618d1..af04687a 100644
--- a/mopidy/models/fields.py
+++ b/mopidy/models/fields.py
@@ -88,14 +88,17 @@ class Date(String):
class Identifier(String):
"""
- :class:`Field` for storing ASCII values such as GUIDs or other identifiers.
+ :class:`Field` for storing values such as GUIDs or other identifiers.
Values will be interned.
:param default: default value for field
"""
def validate(self, value):
- return compat.intern(str(super(Identifier, self).validate(value)))
+ value = super(Identifier, self).validate(value)
+ if isinstance(value, compat.text_type):
+ value = value.encode('utf-8')
+ return compat.intern(value)
class URI(Identifier):
diff --git a/mopidy/mpd/protocol/playback.py b/mopidy/mpd/protocol/playback.py
index 7b943930..08e7cded 100644
--- a/mopidy/mpd/protocol/playback.py
+++ b/mopidy/mpd/protocol/playback.py
@@ -325,7 +325,7 @@ def replay_gain_status(context):
Prints replay gain options. Currently, only the variable
``replay_gain_mode`` is returned.
"""
- return 'off' # TODO
+ return 'replay_gain_mode: off' # TODO
@protocol.commands.add('seek', songpos=protocol.UINT, seconds=protocol.UINT)
diff --git a/mopidy/mpd/protocol/status.py b/mopidy/mpd/protocol/status.py
index 16e9d013..3d76d35f 100644
--- a/mopidy/mpd/protocol/status.py
+++ b/mopidy/mpd/protocol/status.py
@@ -173,6 +173,7 @@ def status(context):
decimal places for millisecond precision.
"""
tl_track = context.core.playback.get_current_tl_track()
+ next_tlid = context.core.tracklist.get_next_tlid()
futures = {
'tracklist.length': context.core.tracklist.get_length(),
@@ -185,6 +186,9 @@ def status(context):
'playback.state': context.core.playback.get_state(),
'playback.current_tl_track': tl_track,
'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(),
}
pykka.get_all(futures.values())
@@ -199,10 +203,12 @@ def status(context):
('xfade', _status_xfade(futures)),
('state', _status_state(futures)),
]
- # TODO: add nextsong and nextsongid
if futures['playback.current_tl_track'].get() is not None:
result.append(('song', _status_songpos(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 (
PlaybackState.PLAYING, PlaybackState.PAUSED):
result.append(('time', _status_time(futures)))
@@ -259,6 +265,14 @@ def _status_songpos(futures):
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):
state = futures['playback.state'].get()
if state == PlaybackState.PLAYING:
diff --git a/mopidy/stream/actor.py b/mopidy/stream/actor.py
index 0861b5b0..1bdd05ca 100644
--- a/mopidy/stream/actor.py
+++ b/mopidy/stream/actor.py
@@ -68,7 +68,7 @@ class StreamLibraryProvider(backend.LibraryProvider):
track = tags.convert_tags_to_track(scan_result.tags).replace(
uri=uri, length=scan_result.duration)
else:
- logger.warning('Problem looking up %s: %s', uri)
+ logger.warning('Problem looking up %s', uri)
track = Track(uri=uri)
return [track]
@@ -142,7 +142,7 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
uri, timeout)
return None, None
content = http.download(
- requests_session, uri, timeout=download_timeout)
+ requests_session, uri, timeout=download_timeout / 1000)
if content is None:
logger.info(
diff --git a/tests/audio/test_tags.py b/tests/audio/test_tags.py
index 01475124..d85bcc12 100644
--- a/tests/audio/test_tags.py
+++ b/tests/audio/test_tags.py
@@ -44,6 +44,14 @@ class TestConvertTaglist(object):
assert isinstance(result[Gst.TAG_DATE][0], compat.text_type)
assert result[Gst.TAG_DATE][0] == '2014-01-07'
+ def test_date_tag_bad_value(self):
+ date = GLib.Date.new_dmy(7, 1, 10000)
+ taglist = self.make_taglist(Gst.TAG_DATE, [date])
+
+ result = tags.convert_taglist(taglist)
+
+ assert len(result[Gst.TAG_DATE]) == 0
+
def test_date_time_tag(self):
taglist = self.make_taglist(Gst.TAG_DATE_TIME, [
Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12')
diff --git a/tests/core/test_playback.py b/tests/core/test_playback.py
index e63609bf..958e0aaf 100644
--- a/tests/core/test_playback.py
+++ b/tests/core/test_playback.py
@@ -735,6 +735,7 @@ class EventEmissionTest(BaseTest):
self.core.playback.play(tl_tracks[0])
self.trigger_about_to_finish(replay_until='stream_changed')
+ self.replay_events()
listener_mock.reset_mock()
self.core.playback.seek(1000)
diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py
index 825f66c6..0a69f564 100644
--- a/tests/models/test_fields.py
+++ b/tests/models/test_fields.py
@@ -1,8 +1,11 @@
+# encoding: utf-8
+
from __future__ import absolute_import, unicode_literals
import unittest
-from mopidy.models.fields import * # noqa: F403
+from mopidy.models.fields import (Boolean, Collection, Field, Identifier,
+ Integer, String)
def create_instance(field):
@@ -126,6 +129,42 @@ class StringTest(unittest.TestCase):
self.assertEqual('', instance.attr)
+class IdentifierTest(unittest.TestCase):
+ def test_default_handling(self):
+ instance = create_instance(Identifier(default='abc'))
+ self.assertEqual('abc', instance.attr)
+
+ def test_native_str_allowed(self):
+ instance = create_instance(Identifier())
+ instance.attr = str('abc')
+ self.assertEqual('abc', instance.attr)
+
+ def test_bytes_allowed(self):
+ instance = create_instance(Identifier())
+ instance.attr = b'abc'
+ self.assertEqual(b'abc', instance.attr)
+
+ def test_unicode_allowed(self):
+ instance = create_instance(Identifier())
+ instance.attr = u'abc'
+ self.assertEqual(u'abc', instance.attr)
+
+ def test_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):
def test_default_handling(self):
instance = create_instance(Integer(default=1234))
diff --git a/tests/mpd/protocol/test_playback.py b/tests/mpd/protocol/test_playback.py
index 9f13fc22..07ece3b0 100644
--- a/tests/mpd/protocol/test_playback.py
+++ b/tests/mpd/protocol/test_playback.py
@@ -115,7 +115,7 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_replay_gain_status_default(self):
self.send_request('replay_gain_status')
self.assertInResponse('OK')
- self.assertInResponse('off')
+ self.assertInResponse('replay_gain_mode: off')
def test_mixrampdb(self):
self.send_request('mixrampdb "10"')
diff --git a/tests/mpd/test_status.py b/tests/mpd/test_status.py
index 25b8dd72..9450808c 100644
--- a/tests/mpd/test_status.py
+++ b/tests/mpd/test_status.py
@@ -48,9 +48,9 @@ class StatusHandlerTest(unittest.TestCase):
def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all()
- def set_tracklist(self, track):
- self.backend.library.dummy_library = [track]
- self.core.tracklist.add(uris=[track.uri]).get()
+ def set_tracklist(self, tracks):
+ self.backend.library.dummy_library = tracks
+ self.core.tracklist.add(uris=[track.uri for track in tracks]).get()
def test_stats_method(self):
result = status.stats(self.context)
@@ -154,22 +154,35 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['state'], 'pause')
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()
result = dict(status.status(self.context))
self.assertIn('song', result)
self.assertGreaterEqual(int(result['song']), 0)
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()
result = dict(status.status(self.context))
self.assertIn('songid', result)
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):
- self.set_tracklist(Track(uri='dummy:/a', length=None))
+ self.set_tracklist([Track(uri='dummy:/a', length=None)])
self.core.playback.play().get()
result = dict(status.status(self.context))
self.assertIn('time', result)
@@ -179,7 +192,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertLessEqual(position, total)
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()
result = dict(status.status(self.context))
self.assertIn('time', result)
@@ -189,7 +202,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertLessEqual(position, total)
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.pause()
self.core.playback.seek(59123)
@@ -198,7 +211,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['elapsed'], '59.123')
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.pause()
result = dict(status.status(self.context))
@@ -206,7 +219,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(result['elapsed'], '0.000')
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()
result = dict(status.status(self.context))
self.assertIn('bitrate', result)
diff --git a/tox.ini b/tox.ini
index da6bcc38..b39fc68b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -37,7 +37,8 @@ commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
[testenv:flake8]
deps =
flake8
- flake8-import-order
+# TODO: Re-enable once https://github.com/PyCQA/flake8-import-order/issues/79 is released.
+# flake8-import-order
pep8-naming
commands = flake8 --show-source --statistics mopidy tests