Release v2.0.1
This commit is contained in:
commit
7018c03e30
@ -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
|
||||||
|
|||||||
@ -5,6 +5,64 @@ Changelog
|
|||||||
This changelog is used to track all major changes to Mopidy.
|
This changelog is used to track all major changes to Mopidy.
|
||||||
|
|
||||||
|
|
||||||
|
v2.0.1 (2016-08-16)
|
||||||
|
===================
|
||||||
|
|
||||||
|
Bug fix release.
|
||||||
|
|
||||||
|
- Audio: Set ``soft-volume`` flag on GStreamer's playbin element. This is the
|
||||||
|
playbin's default, but we managed to override it when configuring the playbin
|
||||||
|
to only process audio. This should fix the "Volume/mute is not available"
|
||||||
|
warning.
|
||||||
|
|
||||||
|
- Audio: Fix buffer conversion. This fixes image extraction.
|
||||||
|
(Fixes: :issue:`1469`, PR: :issue:`1472`)
|
||||||
|
|
||||||
|
- 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`, :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`)
|
||||||
|
|
||||||
|
- 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)
|
v2.0.0 (2016-02-15)
|
||||||
===================
|
===================
|
||||||
|
|
||||||
@ -343,7 +401,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
|
||||||
@ -2046,10 +2104,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**
|
||||||
|
|
||||||
@ -2123,7 +2181,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.
|
||||||
|
|
||||||
@ -2153,7 +2211,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.
|
||||||
@ -2186,11 +2244,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
|
||||||
@ -2200,8 +2258,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.
|
||||||
|
|
||||||
@ -2250,7 +2308,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.
|
||||||
|
|
||||||
@ -2531,9 +2589,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
|
||||||
@ -2658,7 +2715,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.
|
||||||
|
|
||||||
@ -2870,9 +2927,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
|
||||||
@ -3152,12 +3209,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``,
|
||||||
@ -3300,8 +3357,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:
|
||||||
|
|
||||||
@ -3529,11 +3586,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`.
|
||||||
|
|
||||||
@ -3599,7 +3656,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.
|
||||||
@ -3781,7 +3838,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.
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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::
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
Sphinx >= 1.0
|
Sphinx >= 1.0
|
||||||
pygraphviz
|
pygraphviz
|
||||||
|
Pykka >= 1.1
|
||||||
|
sphinx_rtd_theme
|
||||||
|
|||||||
@ -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.
|
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# set_state() on a pipeline.
|
# set_state() on a pipeline.
|
||||||
gst_logger = logging.getLogger('mopidy.audio.gst')
|
gst_logger = logging.getLogger('mopidy.audio.gst')
|
||||||
|
|
||||||
|
_GST_PLAY_FLAGS_AUDIO = 0x02
|
||||||
|
|
||||||
_GST_STATE_MAPPING = {
|
_GST_STATE_MAPPING = {
|
||||||
Gst.State.PLAYING: PlaybackState.PLAYING,
|
Gst.State.PLAYING: PlaybackState.PLAYING,
|
||||||
Gst.State.PAUSED: PlaybackState.PAUSED,
|
Gst.State.PAUSED: PlaybackState.PAUSED,
|
||||||
@ -366,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: '
|
||||||
@ -410,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
|
||||||
@ -448,7 +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('flags', 2) # GST_PLAY_FLAG_AUDIO
|
playbin.set_property('flags', _GST_PLAY_FLAGS_AUDIO)
|
||||||
|
|
||||||
# TODO: turn into config values...
|
# TODO: turn into config values...
|
||||||
playbin.set_property('buffer-size', 5 << 20) # 5MB
|
playbin.set_property('buffer-size', 5 << 20) # 5MB
|
||||||
@ -485,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',
|
||||||
@ -501,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)
|
||||||
|
audio_sink.add(volume)
|
||||||
|
|
||||||
|
queue.link(volume)
|
||||||
|
volume.link(self._outputs)
|
||||||
|
|
||||||
if self.mixer:
|
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)
|
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)
|
||||||
@ -803,8 +809,10 @@ 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._playbin.send_event(event)
|
self._pending_metadata = event
|
||||||
|
else:
|
||||||
|
self._playbin.send_event(event)
|
||||||
|
|
||||||
def get_current_tags(self):
|
def get_current_tags(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -61,8 +61,7 @@ class Scanner(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_start_pipeline(pipeline)
|
_start_pipeline(pipeline)
|
||||||
tags, mime, have_audio = _process(pipeline, timeout)
|
tags, mime, have_audio, duration = _process(pipeline, timeout)
|
||||||
duration = _query_duration(pipeline)
|
|
||||||
seekable = _query_seekable(pipeline)
|
seekable = _query_seekable(pipeline)
|
||||||
finally:
|
finally:
|
||||||
signals.clear()
|
signals.clear()
|
||||||
@ -136,28 +135,15 @@ def _start_pipeline(pipeline):
|
|||||||
pipeline.set_state(Gst.State.PLAYING)
|
pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
|
|
||||||
def _query_duration(pipeline, timeout=100):
|
def _query_duration(pipeline):
|
||||||
# 1. Try and get a duration, return if success.
|
|
||||||
# 2. Some formats need to play some buffers before duration is found.
|
|
||||||
# 3. Wait for a duration change event.
|
|
||||||
# 4. Try and get a duration again.
|
|
||||||
|
|
||||||
success, duration = pipeline.query_duration(Gst.Format.TIME)
|
success, duration = pipeline.query_duration(Gst.Format.TIME)
|
||||||
if success and duration >= 0:
|
if not success:
|
||||||
return duration // Gst.MSECOND
|
duration = None # Make sure error case preserves None.
|
||||||
|
elif duration < 0:
|
||||||
result = pipeline.set_state(Gst.State.PLAYING)
|
duration = None # Stream without duration.
|
||||||
if result == Gst.StateChangeReturn.FAILURE:
|
else:
|
||||||
return None
|
duration = duration // Gst.MSECOND
|
||||||
|
return success, duration
|
||||||
gst_timeout = timeout * Gst.MSECOND
|
|
||||||
bus = pipeline.get_bus()
|
|
||||||
bus.timed_pop_filtered(gst_timeout, Gst.MessageType.DURATION_CHANGED)
|
|
||||||
|
|
||||||
success, duration = pipeline.query_duration(Gst.Format.TIME)
|
|
||||||
if success and duration >= 0:
|
|
||||||
return duration // Gst.MSECOND
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _query_seekable(pipeline):
|
def _query_seekable(pipeline):
|
||||||
@ -172,6 +158,7 @@ def _process(pipeline, timeout_ms):
|
|||||||
mime = None
|
mime = None
|
||||||
have_audio = False
|
have_audio = False
|
||||||
missing_message = None
|
missing_message = None
|
||||||
|
duration = None
|
||||||
|
|
||||||
types = (
|
types = (
|
||||||
Gst.MessageType.ELEMENT |
|
Gst.MessageType.ELEMENT |
|
||||||
@ -179,11 +166,12 @@ def _process(pipeline, timeout_ms):
|
|||||||
Gst.MessageType.ERROR |
|
Gst.MessageType.ERROR |
|
||||||
Gst.MessageType.EOS |
|
Gst.MessageType.EOS |
|
||||||
Gst.MessageType.ASYNC_DONE |
|
Gst.MessageType.ASYNC_DONE |
|
||||||
|
Gst.MessageType.DURATION_CHANGED |
|
||||||
Gst.MessageType.TAG
|
Gst.MessageType.TAG
|
||||||
)
|
)
|
||||||
|
|
||||||
timeout = timeout_ms
|
timeout = timeout_ms
|
||||||
previous = int(time.time() * 1000)
|
start = int(time.time() * 1000)
|
||||||
while timeout > 0:
|
while timeout > 0:
|
||||||
message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types)
|
message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types)
|
||||||
|
|
||||||
@ -197,7 +185,7 @@ def _process(pipeline, timeout_ms):
|
|||||||
mime = message.get_structure().get_value('caps').get_name()
|
mime = message.get_structure().get_value('caps').get_name()
|
||||||
if mime and (
|
if mime and (
|
||||||
mime.startswith('text/') or mime == 'application/xml'):
|
mime.startswith('text/') or mime == 'application/xml'):
|
||||||
return tags, mime, have_audio
|
return tags, mime, have_audio, duration
|
||||||
elif message.get_structure().get_name() == 'have-audio':
|
elif message.get_structure().get_name() == 'have-audio':
|
||||||
have_audio = True
|
have_audio = True
|
||||||
elif message.type == Gst.MessageType.ERROR:
|
elif message.type == Gst.MessageType.ERROR:
|
||||||
@ -205,21 +193,38 @@ def _process(pipeline, timeout_ms):
|
|||||||
if missing_message and not mime:
|
if missing_message and not mime:
|
||||||
caps = missing_message.get_structure().get_value('detail')
|
caps = missing_message.get_structure().get_value('detail')
|
||||||
mime = caps.get_structure(0).get_name()
|
mime = caps.get_structure(0).get_name()
|
||||||
return tags, mime, have_audio
|
return tags, mime, have_audio, duration
|
||||||
raise exceptions.ScannerError(error)
|
raise exceptions.ScannerError(error)
|
||||||
elif message.type == Gst.MessageType.EOS:
|
elif message.type == Gst.MessageType.EOS:
|
||||||
return tags, mime, have_audio
|
return tags, mime, have_audio, duration
|
||||||
elif message.type == Gst.MessageType.ASYNC_DONE:
|
elif message.type == Gst.MessageType.ASYNC_DONE:
|
||||||
if message.src == pipeline:
|
success, duration = _query_duration(pipeline)
|
||||||
return tags, mime, have_audio
|
if tags and success:
|
||||||
|
return tags, mime, have_audio, duration
|
||||||
|
|
||||||
|
# Workaround for upstream bug which causes tags/duration to arrive
|
||||||
|
# after pre-roll. We get around this by starting to play the track
|
||||||
|
# and then waiting for a duration change.
|
||||||
|
# https://bugzilla.gnome.org/show_bug.cgi?id=763553
|
||||||
|
result = pipeline.set_state(Gst.State.PLAYING)
|
||||||
|
if result == Gst.StateChangeReturn.FAILURE:
|
||||||
|
return tags, mime, have_audio, duration
|
||||||
|
|
||||||
|
elif message.type == Gst.MessageType.DURATION_CHANGED:
|
||||||
|
# duration will be read after ASYNC_DONE received; for now
|
||||||
|
# just give it a non-None value to flag that we have a duration:
|
||||||
|
duration = 0
|
||||||
elif message.type == Gst.MessageType.TAG:
|
elif message.type == Gst.MessageType.TAG:
|
||||||
taglist = message.parse_tag()
|
taglist = message.parse_tag()
|
||||||
# Note that this will only keep the last tag.
|
# Note that this will only keep the last tag.
|
||||||
tags.update(tags_lib.convert_taglist(taglist))
|
tags.update(tags_lib.convert_taglist(taglist))
|
||||||
|
|
||||||
now = int(time.time() * 1000)
|
timeout = timeout_ms - (int(time.time() * 1000) - start)
|
||||||
timeout -= now - previous
|
|
||||||
previous = now
|
# workaround for https://bugzilla.gnome.org/show_bug.cgi?id=763553:
|
||||||
|
# if we got what we want then stop playing (and wait for ASYNC_DONE)
|
||||||
|
if tags and duration is not None:
|
||||||
|
pipeline.set_state(Gst.State.PAUSED)
|
||||||
|
|
||||||
raise exceptions.ScannerError('Timeout after %dms' % timeout_ms)
|
raise exceptions.ScannerError('Timeout after %dms' % timeout_ms)
|
||||||
|
|
||||||
|
|||||||
@ -44,15 +44,24 @@ 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):
|
||||||
date = datetime.date(
|
try:
|
||||||
value.get_year(), value.get_month(), value.get_day())
|
date = datetime.date(
|
||||||
result[tag].append(date.isoformat().decode('utf-8'))
|
value.get_year(), value.get_month(), value.get_day())
|
||||||
if isinstance(value, Gst.DateTime):
|
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'))
|
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'))
|
||||||
elif isinstance(value, (compat.text_type, bool, numbers.Number)):
|
elif isinstance(value, (compat.text_type, bool, numbers.Number)):
|
||||||
result[tag].append(value)
|
result[tag].append(value)
|
||||||
|
elif isinstance(value, Gst.Sample):
|
||||||
|
data = _extract_sample_data(value)
|
||||||
|
if data:
|
||||||
|
result[tag].append(data)
|
||||||
else:
|
else:
|
||||||
logger.log(
|
logger.log(
|
||||||
log.TRACE_LOG_LEVEL,
|
log.TRACE_LOG_LEVEL,
|
||||||
@ -62,6 +71,13 @@ gstreamer-GstTagList.html
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_sample_data(sample):
|
||||||
|
buf = sample.get_buffer()
|
||||||
|
if not buf:
|
||||||
|
return None
|
||||||
|
return buf.extract_dup(0, buf.get_size())
|
||||||
|
|
||||||
|
|
||||||
# TODO: split based on "stream" and "track" based conversion? i.e. handle data
|
# TODO: split based on "stream" and "track" based conversion? i.e. handle data
|
||||||
# from radios in it's own helper instead?
|
# from radios in it's own helper instead?
|
||||||
def convert_tags_to_track(tags):
|
def convert_tags_to_track(tags):
|
||||||
@ -120,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):
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -230,8 +230,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
|
||||||
|
|
||||||
def _on_about_to_finish_callback(self):
|
def _on_about_to_finish_callback(self):
|
||||||
@ -251,23 +251,34 @@ class PlaybackController(object):
|
|||||||
if self._state == PlaybackState.STOPPED:
|
if self._state == PlaybackState.STOPPED:
|
||||||
return
|
return
|
||||||
|
|
||||||
pending = self.core.tracklist.eot_track(self._current_tl_track)
|
# Unless overridden by other calls (e.g. next / previous / stop) this
|
||||||
while pending:
|
# will be the last position recorded until the track gets reassigned.
|
||||||
# TODO: Avoid infinite loops if all tracks are unplayable.
|
# TODO: Check if case when track.length isn't populated needs to be
|
||||||
backend = self._get_backend(pending)
|
# handled.
|
||||||
if not backend:
|
self._last_position = self._current_tl_track.track.length
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
pending = self.core.tracklist.eot_track(self._current_tl_track)
|
||||||
if backend.playback.change_track(pending.track).get():
|
# avoid endless loop if 'repeat' is 'true' and no track is playable
|
||||||
self._pending_tl_track = pending
|
# * 2 -> second run to get all playable track in a shuffled playlist
|
||||||
break
|
count = self.core.tracklist.get_length() * 2
|
||||||
except Exception:
|
|
||||||
logger.exception('%s backend caused an exception.',
|
while pending:
|
||||||
backend.actor_ref.actor_class.__name__)
|
backend = self._get_backend(pending)
|
||||||
|
if backend:
|
||||||
|
try:
|
||||||
|
if backend.playback.change_track(pending.track).get():
|
||||||
|
self._pending_tl_track = pending
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
logger.exception('%s backend caused an exception.',
|
||||||
|
backend.actor_ref.actor_class.__name__)
|
||||||
|
|
||||||
self.core.tracklist._mark_unplayable(pending)
|
self.core.tracklist._mark_unplayable(pending)
|
||||||
pending = self.core.tracklist.eot_track(pending)
|
pending = self.core.tracklist.eot_track(pending)
|
||||||
|
count -= 1
|
||||||
|
if not count:
|
||||||
|
logger.info('No playable track in the list.')
|
||||||
|
break
|
||||||
|
|
||||||
def _on_tracklist_change(self):
|
def _on_tracklist_change(self):
|
||||||
"""
|
"""
|
||||||
@ -290,6 +301,9 @@ class PlaybackController(object):
|
|||||||
"""
|
"""
|
||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
current = self._pending_tl_track or self._current_tl_track
|
current = self._pending_tl_track or 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
|
||||||
|
count = self.core.tracklist.get_length() * 2
|
||||||
|
|
||||||
while current:
|
while current:
|
||||||
pending = self.core.tracklist.next_track(current)
|
pending = self.core.tracklist.next_track(current)
|
||||||
@ -301,6 +315,10 @@ class PlaybackController(object):
|
|||||||
# if current == pending:
|
# if current == pending:
|
||||||
# break
|
# break
|
||||||
current = pending
|
current = pending
|
||||||
|
count -= 1
|
||||||
|
if not count:
|
||||||
|
logger.info('No playable track in the list.')
|
||||||
|
break
|
||||||
|
|
||||||
# TODO return result?
|
# TODO return result?
|
||||||
|
|
||||||
@ -352,6 +370,9 @@ class PlaybackController(object):
|
|||||||
|
|
||||||
current = self._pending_tl_track or self._current_tl_track
|
current = self._pending_tl_track or self._current_tl_track
|
||||||
pending = tl_track or current or self.core.tracklist.next_track(None)
|
pending = tl_track or current or self.core.tracklist.next_track(None)
|
||||||
|
# avoid endless loop if 'repeat' is 'true' and no track is playable
|
||||||
|
# * 2 -> second run to get all playable track in a shuffled playlist
|
||||||
|
count = self.core.tracklist.get_length() * 2
|
||||||
|
|
||||||
while pending:
|
while pending:
|
||||||
if self._change(pending, PlaybackState.PLAYING):
|
if self._change(pending, PlaybackState.PLAYING):
|
||||||
@ -360,6 +381,10 @@ class PlaybackController(object):
|
|||||||
self.core.tracklist._mark_unplayable(pending)
|
self.core.tracklist._mark_unplayable(pending)
|
||||||
current = pending
|
current = pending
|
||||||
pending = self.core.tracklist.next_track(current)
|
pending = self.core.tracklist.next_track(current)
|
||||||
|
count -= 1
|
||||||
|
if not count:
|
||||||
|
logger.info('No playable track in the list.')
|
||||||
|
break
|
||||||
|
|
||||||
# TODO return result?
|
# TODO return result?
|
||||||
|
|
||||||
@ -375,6 +400,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()
|
||||||
|
|
||||||
@ -416,6 +445,9 @@ class PlaybackController(object):
|
|||||||
self._previous = True
|
self._previous = True
|
||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
current = self._pending_tl_track or self._current_tl_track
|
current = self._pending_tl_track or 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
|
||||||
|
count = self.core.tracklist.get_length() * 2
|
||||||
|
|
||||||
while current:
|
while current:
|
||||||
pending = self.core.tracklist.previous_track(current)
|
pending = self.core.tracklist.previous_track(current)
|
||||||
@ -427,6 +459,10 @@ class PlaybackController(object):
|
|||||||
# if current == pending:
|
# if current == pending:
|
||||||
# break
|
# break
|
||||||
current = pending
|
current = pending
|
||||||
|
count -= 1
|
||||||
|
if not count:
|
||||||
|
logger.info('No playable track in the list.')
|
||||||
|
break
|
||||||
|
|
||||||
# TODO: no return value?
|
# TODO: no return value?
|
||||||
|
|
||||||
|
|||||||
@ -132,5 +132,6 @@ class FileLibraryProvider(backend.LibraryProvider):
|
|||||||
|
|
||||||
def _is_in_basedir(self, local_path):
|
def _is_in_basedir(self, local_path):
|
||||||
return any(
|
return any(
|
||||||
path.is_path_inside_base_dir(local_path, media_dir['path'])
|
path.is_path_inside_base_dir(
|
||||||
|
local_path, media_dir['path'].encode('utf-8'))
|
||||||
for media_dir in self._media_dirs)
|
for media_dir in self._media_dirs)
|
||||||
|
|||||||
@ -196,6 +196,11 @@ def find_mtimes(root, follow=False):
|
|||||||
|
|
||||||
|
|
||||||
def is_path_inside_base_dir(path, base_path):
|
def is_path_inside_base_dir(path, base_path):
|
||||||
|
if not isinstance(path, bytes):
|
||||||
|
raise ValueError('path is not a bytestring')
|
||||||
|
if not isinstance(base_path, bytes):
|
||||||
|
raise ValueError('base_path is not a bytestring')
|
||||||
|
|
||||||
if path.endswith(os.sep):
|
if path.endswith(os.sep):
|
||||||
raise ValueError('Path %s cannot end with a path separator'
|
raise ValueError('Path %s cannot end with a path separator'
|
||||||
% path)
|
% path)
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -14,11 +14,42 @@ from tests import dummy_audio
|
|||||||
|
|
||||||
|
|
||||||
class TestPlaybackProvider(backend.PlaybackProvider):
|
class TestPlaybackProvider(backend.PlaybackProvider):
|
||||||
|
|
||||||
|
def __init__(self, audio, backend):
|
||||||
|
super(TestPlaybackProvider, self).__init__(audio, backend)
|
||||||
|
self._call_limit = 10
|
||||||
|
self._call_count = 0
|
||||||
|
self._call_onetime = False
|
||||||
|
|
||||||
|
def reset_call_limit(self):
|
||||||
|
self._call_count = 0
|
||||||
|
self._call_onetime = False
|
||||||
|
|
||||||
|
def is_call_limit_reached(self):
|
||||||
|
return self._call_count > self._call_limit
|
||||||
|
|
||||||
|
def _translate_uri_call_limit(self, uri):
|
||||||
|
self._call_count += 1
|
||||||
|
if self._call_count > self._call_limit:
|
||||||
|
# return any url (not 'None') to stop the endless loop
|
||||||
|
return 'assert: call limit reached'
|
||||||
|
if 'limit_never' in uri:
|
||||||
|
# unplayable
|
||||||
|
return None
|
||||||
|
elif 'limit_one' in uri:
|
||||||
|
# one time playable
|
||||||
|
if self._call_onetime:
|
||||||
|
return None
|
||||||
|
self._call_onetime = True
|
||||||
|
return uri
|
||||||
|
|
||||||
def translate_uri(self, uri):
|
def translate_uri(self, uri):
|
||||||
if 'error' in uri:
|
if 'error' in uri:
|
||||||
raise Exception(uri)
|
raise Exception(uri)
|
||||||
elif 'unplayable' in uri:
|
elif 'unplayable' in uri:
|
||||||
return None
|
return None
|
||||||
|
elif 'limit' in uri:
|
||||||
|
return self._translate_uri_call_limit(uri)
|
||||||
else:
|
else:
|
||||||
return uri
|
return uri
|
||||||
|
|
||||||
@ -703,6 +734,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)
|
||||||
@ -1125,3 +1157,77 @@ class TestBug1352Regression(BaseTest):
|
|||||||
|
|
||||||
self.core.history._add_track.assert_called_once_with(self.tracks[1])
|
self.core.history._add_track.assert_called_once_with(self.tracks[1])
|
||||||
self.core.tracklist._mark_playing.assert_called_once_with(tl_tracks[1])
|
self.core.tracklist._mark_playing.assert_called_once_with(tl_tracks[1])
|
||||||
|
|
||||||
|
|
||||||
|
class TestEndlessLoop(BaseTest):
|
||||||
|
|
||||||
|
tracks_play = [
|
||||||
|
Track(uri='dummy:limit_never:a'),
|
||||||
|
Track(uri='dummy:limit_never:b')
|
||||||
|
]
|
||||||
|
|
||||||
|
tracks_other = [
|
||||||
|
Track(uri='dummy:limit_never:a'),
|
||||||
|
Track(uri='dummy:limit_one'),
|
||||||
|
Track(uri='dummy:limit_never:b')
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_play(self):
|
||||||
|
self.core.tracklist.clear()
|
||||||
|
self.core.tracklist.add(self.tracks_play)
|
||||||
|
|
||||||
|
self.backend.playback.reset_call_limit().get()
|
||||||
|
self.core.tracklist.set_repeat(True)
|
||||||
|
|
||||||
|
tl_tracks = self.core.tracklist.get_tl_tracks()
|
||||||
|
self.core.playback.play(tl_tracks[0])
|
||||||
|
self.replay_events()
|
||||||
|
|
||||||
|
self.assertFalse(self.backend.playback.is_call_limit_reached().get())
|
||||||
|
|
||||||
|
def test_next(self):
|
||||||
|
self.core.tracklist.clear()
|
||||||
|
self.core.tracklist.add(self.tracks_other)
|
||||||
|
|
||||||
|
self.backend.playback.reset_call_limit().get()
|
||||||
|
self.core.tracklist.set_repeat(True)
|
||||||
|
|
||||||
|
tl_tracks = self.core.tracklist.get_tl_tracks()
|
||||||
|
self.core.playback.play(tl_tracks[1])
|
||||||
|
self.replay_events()
|
||||||
|
|
||||||
|
self.core.playback.next()
|
||||||
|
self.replay_events()
|
||||||
|
|
||||||
|
self.assertFalse(self.backend.playback.is_call_limit_reached().get())
|
||||||
|
|
||||||
|
def test_previous(self):
|
||||||
|
self.core.tracklist.clear()
|
||||||
|
self.core.tracklist.add(self.tracks_other)
|
||||||
|
|
||||||
|
self.backend.playback.reset_call_limit().get()
|
||||||
|
self.core.tracklist.set_repeat(True)
|
||||||
|
|
||||||
|
tl_tracks = self.core.tracklist.get_tl_tracks()
|
||||||
|
self.core.playback.play(tl_tracks[1])
|
||||||
|
self.replay_events()
|
||||||
|
|
||||||
|
self.core.playback.previous()
|
||||||
|
self.replay_events()
|
||||||
|
|
||||||
|
self.assertFalse(self.backend.playback.is_call_limit_reached().get())
|
||||||
|
|
||||||
|
def test_on_about_to_finish(self):
|
||||||
|
self.core.tracklist.clear()
|
||||||
|
self.core.tracklist.add(self.tracks_other)
|
||||||
|
|
||||||
|
self.backend.playback.reset_call_limit().get()
|
||||||
|
self.core.tracklist.set_repeat(True)
|
||||||
|
|
||||||
|
tl_tracks = self.core.tracklist.get_tl_tracks()
|
||||||
|
self.core.playback.play(tl_tracks[1])
|
||||||
|
self.replay_events()
|
||||||
|
|
||||||
|
self.trigger_about_to_finish()
|
||||||
|
|
||||||
|
self.assertFalse(self.backend.playback.is_call_limit_reached().get())
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from mopidy import compat, exceptions
|
from mopidy import compat, exceptions
|
||||||
from mopidy.internal import path
|
from mopidy.internal import path
|
||||||
from mopidy.internal.gi import GLib
|
from mopidy.internal.gi import GLib
|
||||||
@ -392,6 +394,30 @@ class FindMTimesTest(unittest.TestCase):
|
|||||||
self.assertEqual(errors, {})
|
self.assertEqual(errors, {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsPathInsideBaseDir(object):
|
||||||
|
def test_when_inside(self):
|
||||||
|
assert path.is_path_inside_base_dir(
|
||||||
|
'/æ/øå'.encode('utf-8'),
|
||||||
|
'/æ'.encode('utf-8'))
|
||||||
|
|
||||||
|
def test_when_outside(self):
|
||||||
|
assert not path.is_path_inside_base_dir(
|
||||||
|
'/æ/øå'.encode('utf-8'),
|
||||||
|
'/ø'.encode('utf-8'))
|
||||||
|
|
||||||
|
def test_byte_inside_str_fails(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
path.is_path_inside_base_dir('/æ/øå'.encode('utf-8'), '/æ')
|
||||||
|
|
||||||
|
def test_str_inside_byte_fails(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
path.is_path_inside_base_dir('/æ/øå', '/æ'.encode('utf-8'))
|
||||||
|
|
||||||
|
def test_str_inside_str_fails(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
path.is_path_inside_base_dir('/æ/øå', '/æ')
|
||||||
|
|
||||||
|
|
||||||
# TODO: kill this in favour of just os.path.getmtime + mocks
|
# TODO: kill this in favour of just os.path.getmtime + mocks
|
||||||
class MtimeTest(unittest.TestCase):
|
class MtimeTest(unittest.TestCase):
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
|
# 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 Collection, Field, Identifier, Integer, String
|
||||||
|
|
||||||
|
|
||||||
def create_instance(field):
|
def create_instance(field):
|
||||||
@ -126,6 +128,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))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user