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