Release v1.1.2

This commit is contained in:
Stein Magnus Jodal 2016-01-18 22:54:57 +01:00
commit 2ba5bdc822
39 changed files with 619 additions and 503 deletions

3
.gitignore vendored
View File

@ -3,6 +3,7 @@
*.pyc
*.swp
*~
.cache/
.coverage
.idea
.noseids
@ -15,5 +16,5 @@ dist/
docs/_build/
mopidy.log*
nosetests.xml
xunit-*.xml
tmp/
xunit-*.xml

View File

@ -55,20 +55,28 @@ Data model API
:synopsis: Data model API
.. autoclass:: mopidy.models.Ref
:members:
.. autoclass:: mopidy.models.Track
:members:
.. autoclass:: mopidy.models.Album
:members:
.. autoclass:: mopidy.models.Artist
:members:
.. autoclass:: mopidy.models.Playlist
:members:
.. autoclass:: mopidy.models.Image
:members:
.. autoclass:: mopidy.models.TlTrack
:members:
.. autoclass:: mopidy.models.SearchResult
:members:
Data model helpers

View File

@ -4,7 +4,7 @@
Authors
*******
Mopidy is copyright 2009-2015 Stein Magnus Jodal and contributors. Mopidy is
Mopidy is copyright 2009-2016 Stein Magnus Jodal and contributors. Mopidy is
licensed under the `Apache License, Version 2.0
<http://www.apache.org/licenses/LICENSE-2.0>`_.

View File

@ -4,6 +4,48 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v1.1.2 (2016-01-18)
===================
Bug fix release.
- Main: Catch errors when loading the :confval:`logging/config_file` file.
(Fixes: :issue:`1320`)
- Core: If changing to another track while the player is paused, the new track
would not be added to the history or marked as currently playing. (Fixes:
:issue:`1352`, PR: :issue:`1356`)
- Core: Skips over unplayable tracks if the user attempts to change tracks
while paused, like we already did if in playing state. (Fixes :issue:`1378`,
PR: :issue:`1379`)
- Core: Make :meth:`~mopidy.core.LibraryController.lookup` ignore tracks with
empty URIs. (Partly fixes: :issue:`1340`, PR: :issue:`1381`)
- Core: Fix crash if backends emits events with wrong names or arguments.
(Fixes: :issue:`1383`)
- Stream: If an URI is considered playable, don't consider it as a candidate
for playlist parsing. Just looking at MIME type prefixes isn't enough, as for
example Ogg Vorbis has the MIME type ``application/ogg``. (Fixes:
:issue:`1299`)
- Local: If the scan or clear commands are used on a library that does not
exist, exit with an error. (Fixes: :issue:`1298`)
- MPD: Notify idling clients when a seek is performed. (Fixes: :issue:`1331`)
- MPD: Don't return tracks with empty URIs. (Partly fixes: :issue:`1340`, PR:
:issue:`1343`)
- MPD: Add ``volume`` command that was reintroduced, though still as a
deprecated command, in MPD 0.18 and is in use by some clients like mpc.
(Fixes: :issue:`1393`, PR: :issue:`1397`)
- Proxy: Handle case where :confval:`proxy/port` is either missing from config
or set to an empty string. (PR: :issue:`1371`)
v1.1.1 (2015-09-14)
===================

View File

@ -93,14 +93,14 @@ source_suffix = '.rst'
master_doc = 'index'
project = 'Mopidy'
copyright = '2009-2015, Stein Magnus Jodal and contributors'
copyright = '2009-2016, Stein Magnus Jodal and contributors'
from mopidy.internal.versioning import get_version
release = get_version()
version = '.'.join(release.split('.')[:2])
# To make the build reproducible, avoid using today's date in the manpages
today = '2015'
today = '2016'
exclude_trees = ['_build']

View File

@ -1,129 +0,0 @@
.. _debian:
***************
Debian packages
***************
The Mopidy Debian package, ``mopidy``, is available from `apt.mopidy.com
<http://apt.mopidy.com/>`__ as well as from Debian, Ubuntu and other
Debian-based Linux distributions.
Some extensions are also available from all of these sources, while others,
like Mopidy-Spotify and its dependencies, are only available from
apt.mopidy.com. This may either be temporary until the package is uploaded to
Debian and with time propagates to the other distributions. It may also be more
long term, like in the Mopidy-Spotify case where there is uncertainities around
licensing and distribution of non-free packages.
Installation
============
See :ref:`debian-install`.
Running as a system service
===========================
The Debian package comes with an init script. It starts Mopidy as a system
service running as the ``mopidy`` user, which is created by the package.
The Debian package version 0.18.3-1 and older starts Mopidy as a system
service by default. Version 0.18.3-2 and newer asks if you want to run Mopidy
as a system service, defaulting to not doing so.
If you're running 0.18.3-2 or newer, and you've changed your mind about whether
or not to run Mopidy as a system service, just run the following command to
reconfigure the package::
sudo dpkg-reconfigure mopidy
If you're running 0.18.3-1 or older, and don't want to use the init script to
run Mopidy as a system service, but instead just run Mopidy manually using your
own user, you need to disable the init script and stop Mopidy by running::
sudo update-rc.d mopidy disable
sudo service mopidy stop
This way of disabling the system service is compatible with the improved
0.18.3-2 or newer version of the Debian package, so if you later upgrade to a
newer version, you can change your mind using the ``dpkg-reconfigure`` command
above.
Differences when running as a system service
============================================
If you want to run Mopidy using the init script, there's a few differences
from a regular Mopidy setup you'll want to know about.
- All configuration is in :file:`/etc/mopidy`, not in your user's home
directory. The main configuration file is :file:`/etc/mopidy/mopidy.conf`.
You can do all your changes in this file.
- Mopidy extensions installed from Debian packages will sometimes install
additional configuration files in :file:`/usr/share/mopidy/conf.d/`. These
files just provide different defaults for the extension when run as a system
service. You can override anything from :file:`/usr/share/mopidy/conf.d/` in
the :file:`/etc/mopidy/mopidy.conf` configuration file.
Previously, the extension's default config was installed in
:file:`/etc/mopidy/extensions.d/`. This was removed with the Debian
package mopidy 0.19.4-3. If you have modified any files in
:file:`/etc/mopidy/extensions.d/`, you should redo your modifications in
:file:`/etc/mopidy/mopidy.conf` and delete the
:file:`/etc/mopidy/extensions.d/` directory.
- The init script runs Mopidy as the ``mopidy`` user. The ``mopidy`` user will
need read access to any local music you want Mopidy to play.
- To run Mopidy subcommands with the same user and config files as the init
script uses, you can use ``sudo mopidyctl <subcommand>``. In other words,
where you'll usually run::
mopidy config
You should instead run the following to inspect the system service's
configuration::
sudo mopidyctl config
The same applies to scanning your local music collection. Where you'll
normally run::
mopidy local scan
You should instead run::
sudo mopidyctl local scan
Previously, you used ``sudo service mopidy run <subcommand>`` instead of
``mopidyctl``. This was deprecated in Debian package version 0.19.4-3 in
favor of ``mopidyctl``, which also work for systems using systemd instead of
sysvinit and traditional init scripts.
- Mopidy is started, stopped, and restarted just like any other system
service::
sudo service mopidy start
sudo service mopidy stop
sudo service mopidy restart
- You can check if Mopidy is currently running as a system service by running::
sudo service mopidy status
- Mopidy installed from a Debian package can use Mopidy extensions installed
both from Debian packages and with pip. This has always been the case.
Mopidy installed with pip can use extensions installed with pip, but
not extensions installed from a Debian package released before August 2015.
This is because the Debian packages used to install extensions into
:file:`/usr/share/mopidy` which is normally not on your ``PYTHONPATH``.
Thus, your pip-installed Mopidy would not find the Debian package-installed
extensions.
In August 2015, all Mopidy extension Debian packages was modified to install
into :file:`/usr/lib/python2.7/dist-packages`, like any other Python Debian
package. Thus, Mopidy installed with pip can now use extensions installed
from Debian.

View File

@ -81,8 +81,8 @@ announcements related to Mopidy and Mopidy extensions.
installation/index
config
running
service
troubleshooting
debian
.. _ext:

View File

@ -16,7 +16,8 @@ If you are running Arch Linux, you can install Mopidy using the
pacman -Syu
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.
then you're ready to :doc:`run Mopidy </running>` or run Mopidy as a
:ref:`service <service>`.
Installing extensions

View File

@ -48,12 +48,9 @@ and armhf (compatible with Raspberry Pi 1 and 2).
sudo apt-get update
sudo apt-get install mopidy
#. Before continuing, make sure you've read the :ref:`debian` section to learn
about the differences between running Mopidy as a system service and
manually as your own system user.
#. Finally, you need to set a couple of :doc:`config values </config>`, and then
you're ready to :doc:`run Mopidy </running>`.
you're ready to :doc:`run Mopidy </running>` or run Mopidy as a
:ref:`service <service>`.
When a new release of Mopidy is out, and you can't wait for you system to
figure it out for itself, run the following to upgrade right away::
@ -87,44 +84,3 @@ about any other requirements needed for the extension to work properly.
For a full list of available Mopidy extensions, including those not
installable from apt.mopidy.com, see :ref:`ext`.
Missing extensions
==================
If you've installed a Mopidy extension with pip, restarted Mopidy, and Mopidy
doesn't find the extension, there's probably a simple explanation and solution.
Mopidy installed with APT can detect and use Mopidy extensions installed with
both APT and pip. APT installs Mopidy as :file:`/usr/bin/mopidy`.
Mopidy installed with pip can only detect Mopidy extensions installed with pip.
pip usually installs Mopidy as :file:`/usr/local/bin/mopidy`.
If you have Mopidy installed from both APT and pip, then the pip-installed
Mopidy will probably shadow the APT-installed Mopidy because
:file:`/usr/local/bin` usually has precedence over :file:`/usr/bin` in the
``PATH`` environment variable. To check if this is the case on your system, you
can use ``which`` to see what installation of Mopidy you use when you run
``mopidy`` in your shell::
$ which mopidy
/usr/local/bin/mopidy
If this is the case on your system, the recommended solution is to check that
you have Mopidy installed from APT too::
$ /usr/bin/mopidy --version
Mopidy 0.19.5
And then uninstall the pip-installed Mopidy::
sudo pip uninstall mopidy
Depending on what shell you use, the shell may still try to use
:file:`/usr/local/bin/mopidy` even if it no longer exists. Check again with
``which mopidy`` what your shell believes is the right ``mopidy`` executable to
run. If the shell is still confused, you may need to restart it, or in the case
of zsh, run ``rehash`` to update the shell.
For more details on why this works this way, see :ref:`debian`.

View File

@ -86,6 +86,8 @@ For a full list of available Mopidy extensions, including those not installable
from Homebrew, see :ref:`ext`.
.. _osx-service:
Running Mopidy automatically on login
=====================================

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,75 +1,68 @@
.. _raspberrypi-installation:
*************************************
Raspberry Pi: Mopidy on a credit card
*************************************
************
Raspberry Pi
************
Mopidy runs nicely on a `Raspberry Pi <http://www.raspberrypi.org/>`_. As of
January 2013, Mopidy will run with Spotify support on both the armel
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian
distribution.
Mopidy runs on all versions of `Raspberry Pi <http://www.raspberrypi.org/>`_.
However, note that Raspberry Pi 2 B's CPU is approximately six times as
powerful as Raspberry Pi 1 and Raspberry Pi Zero, so Mopidy will be more joyful
to use on a Raspberry Pi 2.
.. image:: raspberry-pi-by-jwrodgers.jpg
.. image:: raspberrypi2.jpg
:width: 640
:height: 427
:height: 363
.. _raspi-wheezy:
How to for Raspbian "wheezy" and Debian "wheezy"
================================================
How to for Raspbian Jessie
==========================
This guide applies for both:
#. Download the latest Jessie or Jessie Lite disk image from
http://www.raspberrypi.org/downloads/raspbian/.
- Raspbian "wheezy" for armhf (hard-float), and
- Debian "wheezy" for armel (soft-float)
If you're only using your Pi for Mopidy, go with Jessie Lite as you won't
need the full graphical desktop included in the Jessie image.
If you don't know which one to select, go for the armhf variant, as it'll give
you a lot better performance.
#. Flash the Raspbian image you downloaded to your SD card.
#. Download the latest "wheezy" disk image from
http://www.raspberrypi.org/downloads/. This was last tested with the images
from 2013-05-25 for armhf and 2013-05-29 for armel.
See the `Raspberry Pi installation docs
<https://www.raspberrypi.org/documentation/installation/installing-images/README.md>`_
for instructions.
#. Flash the OS image to your SD card. See
http://elinux.org/RPi_Easy_SD_Card_Setup for help.
#. If you connect a monitor and a keyboard, you'll see that the Pi boots right
into the ``raspi-config`` tool.
#. If you have an SD card that's >2 GB, you don't have to resize the file
systems on another computer. Just boot up your Raspberry Pi with the
unaltered partions, and it will boot right into the ``raspi-config`` tool,
which will let you grow the root file system to fill the SD card. This tool
will also allow you do other useful stuff, like turning on the SSH server.
If you boot with only a network cable connected, you'll have to find the IP
address of the Pi yourself, e.g. by looking in the client list on your
router/DHCP server. When you have found the Pi's IP address, you can SSH to
the IP address and login with the user ``pi`` and password ``raspberry``.
Once logged in, run ``sudo raspi-config`` to start the config tool as the
``root`` user.
#. You can login to the default user using username ``pi`` and password
``raspberry``. To become root, just enter ``sudo -i``.
#. Use the ``raspi-config`` tool to setup the basics of your Pi. You might want
to do one or more of the following:
#. To avoid a couple of potential problems with Mopidy, turn on IPv6 support:
- Expand the file system to fill the SD card.
- Change the password of the ``pi`` user.
- Change the time zone.
- Load the IPv6 kernel module now::
Under "Advanced Options":
sudo modprobe ipv6
- Set a hostname.
- Enable SSH if not already enabled.
- If your will use HDMI for display and 3.5mm jack for audio, force the
audio output to the 3.5mm jack. By default it will use HDMI for audio
output if an HDMI cable is connected and the 3.5mm jack if not.
- Add ``ipv6`` to ``/etc/modules`` to ensure the IPv6 kernel module is
loaded on boot::
Once done, select "Finish" and restart your Pi.
echo ipv6 | sudo tee -a /etc/modules
If you want to change any settings later, you can simply rerun ``sudo
raspi-config``.
#. Since I have a HDMI cable connected, but want the sound on the analog sound
connector, I have to run::
sudo amixer cset numid=3 1
to force it to use analog output. ``1`` means analog, ``0`` means auto, and
is the default, while ``2`` means HDMI. You can test sound output
independent of Mopidy by running::
aplay /usr/share/sounds/alsa/Front_Center.wav
If you hear a voice saying "Front Center", then your sound is working.
To make the change to analog output stick, you can add the ``amixer``
command to e.g. ``/etc/rc.local``, which will be executed when the system is
booting.
#. Once you've rebooted and has logged in as the ``pi`` user, you can enter
``sudo -i`` to become ``root``.
#. Install Mopidy and its dependencies as described in :ref:`debian-install`.
@ -79,114 +72,19 @@ you a lot better performance.
starting at boot.
Appendix A: Fixing audio quality issues
=======================================
Testing sound output
====================
As of about April 2013 the following steps should resolve any audio
issues for HDMI and analog without the use of an external USB sound
card.
You can test sound output independent of Mopidy by running::
#. Ensure your system is up to date. On Debian based systems run::
aplay /usr/share/sounds/alsa/Front_Center.wav
sudo apt-get update
sudo apt-get dist-upgrade
If you hear a voice saying "Front Center", then your sound is working.
#. Ensure you have a new enough firmware. On Debian based systems
`rpi-update <https://github.com/Hexxeh/rpi-update>`_
can be used.
If you want to change your audio output setting, simply rerun ``sudo
raspi-config``. Alternatively, you can change the audio output setting
directly by running:
#. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the
following::
pcm.!default {
type hw
card 0
}
ctl.!default {
type hw
card 0
}
Note that if you have an ``~/.asoundrc`` it will overide any global
settings from ``/etc/asound.conf``.
#. For Mopidy to output audio directly to ALSA, instead of Jack which
GStreamer usually defaults to on Raspberry Pi, install the
``gstreamer0.10-alsa`` package::
sudo apt-get install gstreamer0.10-alsa
Then update your ``~/.config/mopidy/mopidy.conf`` to contain::
[audio]
output = alsasink
Following these steps you should be able to get crackle free sound on either
HDMI or analog. Note that you might need to ensure that PulseAudio is no longer
running to get this working nicely.
This recipe has been confirmed as working by a number of users on our issue
tracker and IRC. As a reference, the following versions where used for testing
this, however all newer and some older version are likely to work as we have
not determined the exact revision that fixed this::
$ uname -a
Linux raspberrypi 3.6.11+ #408 PREEMPT Wed Apr 10 20:33:39 BST 2013 armv6l GNU/Linux
$ /opt/vc/bin/vcgencmd version
Apr 25 2013 01:07:36
Copyright (c) 2012 Broadcom
version 386589 (release)
The only remaining known issue is a slight gap in playback at track changes
this is likely due to gapless playback not being implemented and is being
worked on irrespective of Raspberry Pi related work.
Appendix B: Raspbmc not booting
===============================
Due to a dependency version problem where XBMC uses another version of libtag
than what Debian originally ships with, you might have to make some minor
changes for Raspbmc to start properly after installing Mopidy.
If you notice that XBMC is not starting but gets stuck in a loop,
you need to make the following changes::
sudo ln -sf /home/pi/.xbmc-current/xbmc-bin/lib/xbmc/system/libtag.so.1 \
/usr/lib/arm-linux-gnueabihf/libtag.so.1
However, this will not persist the changes. To persist the changes edit
:file:`/etc/ld.so.conf.d/arm-linux-gnueabihf.conf` and add the following at the
top::
/home/pi/.xbmc-current/xbmc-bin/lib/xbmc/system
It's very important to add it at the top of the file as this indicates the
priority of the folder in which to look for shared libraries.
XBMC doesn't play nicely with the system wide installed version of libtag that
got installed together with Mopidy, but rather vendors in its own version.
More info about this issue can be found in `this post
<http://geeks.noeit.com/xbmc-library-dependency-error/>`_.
Please note that if you're running Xbian or another XBMC distribution these
instructions might vary for your system.
Appendix C: Installation on XBian
=================================
Similar to the Raspbmc issue outlined in Appendix B, it's not possible to
install Mopidy on XBian without first resolving a dependency problem between
``gstreamer0.10-plugins-good`` and ``libtag1c2a``. More information can be
found in `this post
<https://github.com/xbianonpi/xbian/issues/378#issuecomment-37723392>`_.
Run the following commands to remedy this and then install Mopidy as normal::
cd /tmp
wget http://apt.xbian.org/pool/stable/rpi-wheezy/l/libtag1c2a/libtag1c2a_1.7.2-1_armhf.deb
sudo dpkg -i libtag1c2a_1.7.2-1_armhf.deb
rm libtag1c2a_1.7.2-1_armhf.deb
- Auto (HDMI if connected, else 3.5mm jack): ``sudo amixer cset numid=3 0``
- Use 3.5mm jack: ``sudo amixer cset numid=3 1``
- Use HDMI: ``sudo amixer cset numid=3 2``

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -39,21 +39,8 @@ using ``pkill``::
pkill mopidy
Init scripts
============
Running as a service
====================
- The ``mopidy`` package at `apt.mopidy.com <http://apt.mopidy.com/>`__ comes
with an `sysvinit init script
<https://github.com/mopidy/mopidy/blob/debian/debian/mopidy.init>`_. For
more details, see the :ref:`debian` section of the docs.
- The ``mopidy`` package in `Arch Linux
<https://www.archlinux.org/packages/community/any/mopidy/>`__ comes with a systemd init
script.
- A blog post by Benjamin Guillet explains how to `Daemonize Mopidy and Launch
It at Login on OS X
<http://www.benjaminguillet.com/blog/2013/08/16/launch-mopidy-at-login-on-os-x/>`_.
- Issue :issue:`266` contains a bunch of init scripts for Mopidy, including
Upstart init scripts.
Once you're done exploring Mopidy and want to run it as a proper service, check
out :ref:`service`.

98
docs/service.rst Normal file
View File

@ -0,0 +1,98 @@
.. _service:
********************
Running as a service
********************
If you want to run Mopidy as a service using either an init script or a systemd
service, there's a few differences from running Mopidy as your own user you'll
want to know about. The following applies to Debian, Ubuntu, Raspbian, and
Arch. Hopefully, other distributions packaging Mopidy will make sure this works
the same way on their distribution.
Configuration
=============
All configuration is in :file:`/etc/mopidy`, not in your user's home directory.
The main configuration file is :file:`/etc/mopidy/mopidy.conf`. If there are
more than one configuration file, this is the configuration file with the
highest priority, so it can override configs from all other config files.
Thus, you can do all your changes in this file.
mopidy user
===========
The init script runs Mopidy as the ``mopidy`` user, which is automatically
created when you install the Mopidy package. The ``mopidy`` user will need read
access to any local music you want Mopidy to play.
Subcommands
===========
To run Mopidy subcommands with the same user and config files as the service
uses, you can use ``sudo mopidyctl <subcommand>``. In other words, where you'll
usually run::
mopidy config
You should instead run the following to inspect the service's configuration::
sudo mopidyctl config
The same applies to scanning your local music collection. Where you'll normally
run::
mopidy local scan
You should instead run::
sudo mopidyctl local scan
Service management with systemd
===============================
On modern systems using systemd you can enable the Mopidy service by running::
sudo systemctl enable mopidy
This will make Mopidy start when the system boots.
Mopidy is started, stopped, and restarted just like any other systemd service::
sudo systemctl start mopidy
sudo systemctl stop mopidy
sudo systemctl restart mopidy
You can check if Mopidy is currently running as a service by running::
sudo systemctl status mopidy
Service management on Debian
============================
On Debian systems (both those using systemd and not) you can enable the Mopidy
service by running::
sudo dpkg-reconfigure mopidy
Mopidy can be started, stopped, and restarted using the ``service`` command::
sudo service mopidy start
sudo service mopidy stop
sudo service mopidy restart
You can check if Mopidy is currently running as a service by running::
sudo service mopidy status
Service on OS X
===============
If you're installing Mopidy on OS X, see :ref:`osx-service`.

View File

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

View File

@ -141,8 +141,9 @@ def _process(pipeline, timeout_ms):
have_audio = False
missing_message = None
types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR
| gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG)
types = (
gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | gst.MESSAGE_ERROR |
gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | gst.MESSAGE_TAG)
previous = clock.get_time()
while timeout > 0:

View File

@ -236,7 +236,9 @@ class LibraryController(object):
result = future.get()
if result is not None:
validation.check_instances(result, models.Track)
results[u] = result
# TODO Consider making Track.uri field mandatory, and
# then remove this filtering of tracks without URIs.
results[u] = [r for r in result if r.uri]
if uri:
return results[uri]

View File

@ -31,7 +31,8 @@ class CoreListener(listener.Listener):
:type event: string
:param kwargs: any other arguments to the specific event handlers
"""
getattr(self, event)(**kwargs)
# Just delegate to parent, entry mostly for docs.
super(CoreListener, self).on_event(event, **kwargs)
def track_playback_paused(self, tl_track, time_position):
"""

View File

@ -207,13 +207,24 @@ class PlaybackController(object):
if old_state == PlaybackState.PLAYING:
self._play(on_error_step=on_error_step)
elif old_state == PlaybackState.PAUSED:
# NOTE: this is just a quick hack to fix #1177 as this code has
# already been killed in the gapless branch.
# NOTE: this is just a quick hack to fix #1177, #1352, and #1378
# as this code has already been killed in the gapless branch.
backend = self._get_backend()
if backend:
backend.playback.prepare_change()
backend.playback.change_track(tl_track.track).get()
self.pause()
success = backend.playback.change_track(tl_track.track).get()
if success:
self.core.tracklist._mark_playing(tl_track)
self.core.history._add_track(tl_track.track)
else:
self.core.tracklist._mark_unplayable(tl_track)
if on_error_step == 1:
# TODO: can cause an endless loop for single track
# repeat.
self.next()
elif on_error_step == -1:
self.previous()
self.pause()
# TODO: this is not really end of track, this is on_need_next_track
def _on_end_of_track(self):

View File

@ -54,7 +54,7 @@ class Extension(object):
def get_config_schema(self):
"""The extension's config validation schema
:returns: :class:`~mopidy.config.schema.ExtensionConfigSchema`
:returns: :class:`~mopidy.config.schemas.ConfigSchema`
"""
schema = config_lib.ConfigSchema(self.ext_name)
schema['enabled'] = config_lib.Boolean()

View File

@ -21,8 +21,8 @@ def format_proxy(proxy_config, auth=True):
if not proxy_config.get('hostname'):
return None
port = proxy_config.get('port', 80)
if port < 0:
port = proxy_config.get('port')
if not port or port < 0:
port = 80
if proxy_config.get('username') and proxy_config.get('password') and auth:

View File

@ -29,7 +29,11 @@ def download(session, uri, timeout=1.0, chunk_size=4096):
'%.3fs', uri, timeout)
return None
except requests.exceptions.InvalidSchema:
logger.warning('%s has an unsupported schema.', uri)
logger.warning('Download of %r failed due to unsupported schema', uri)
return None
except requests.exceptions.RequestException as exc:
logger.warning('Download of %r failed: %s', uri, exc)
logger.debug('Download exception details', exc_info=True)
return None
content = []

View File

@ -19,6 +19,8 @@ LOG_LEVELS = {
TRACE_LOG_LEVEL = 5
logging.addLevelName(TRACE_LOG_LEVEL, 'TRACE')
logger = logging.getLogger(__name__)
class DelayedHandler(logging.Handler):
@ -54,8 +56,12 @@ def setup_logging(config, verbosity_level, save_debug_log):
if config['logging']['config_file']:
# Logging config from file must be read before other handlers are
# added. If not, the other handlers will have no effect.
logging.config.fileConfig(config['logging']['config_file'],
disable_existing_loggers=False)
try:
path = config['logging']['config_file']
logging.config.fileConfig(path, disable_existing_loggers=False)
except Exception as e:
# Catch everything as logging does not specify what can go wrong.
logger.error('Loading logging config %r failed. %s', path, e)
setup_console_logging(config, verbosity_level)
if save_debug_log:

View File

@ -51,4 +51,8 @@ class Listener(object):
:type event: string
:param kwargs: any other arguments to the specific event handlers
"""
getattr(self, event)(**kwargs)
try:
getattr(self, event)(**kwargs)
except Exception:
# Ensure we don't crash the actor due to "bad" events.
logger.exception('Triggering event failed: %s', event)

View File

@ -21,8 +21,8 @@ def _get_library(args, config):
library_name = config['local']['library']
if library_name not in libraries:
logger.warning('Local library %s not found', library_name)
return 1
logger.error('Local library %s not found', library_name)
return None
logger.debug('Using %s as the local library', library_name)
return libraries[library_name](config)
@ -41,6 +41,9 @@ class ClearCommand(commands.Command):
def run(self, args, config):
library = _get_library(args, config)
if library is None:
return 1
prompt = '\nAre you sure you want to clear the library? [y/N] '
if compat.input(prompt).lower() != 'y':
@ -76,6 +79,8 @@ class ScanCommand(commands.Command):
bytes(file_ext.lower()) for file_ext in excluded_file_extensions)
library = _get_library(args, config)
if library is None:
return 1
file_mtimes, file_errors = path.find_mtimes(
media_dir, follow=config['local']['scan_follow_symlinks'])

View File

@ -348,14 +348,14 @@ class SearchResult(ValidatedImmutableObject):
:type albums: list of :class:`Album` elements
"""
# The search result URI. Read-only.
#: The search result URI. Read-only.
uri = fields.URI()
# The tracks matching the search query. Read-only.
#: The tracks matching the search query. Read-only.
tracks = fields.Collection(type=Track, container=tuple)
# The artists matching the search query. Read-only.
#: The artists matching the search query. Read-only.
artists = fields.Collection(type=Artist, container=tuple)
# The albums matching the search query. Read-only.
#: The albums matching the search query. Read-only.
albums = fields.Collection(type=Album, container=tuple)

View File

@ -77,3 +77,6 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
def stream_title_changed(self, title):
self.send_idle('playlist')
def seeked(self, time_position):
self.send_idle('player')

View File

@ -47,6 +47,7 @@ class MpdDispatcher(object):
return self._call_next_filter(request, response, filter_chain)
def handle_idle(self, subsystem):
# TODO: validate against mopidy/mpd/protocol/status.SUBSYSTEMS
self.context.events.add(subsystem)
subsystems = self.context.subscriptions.intersection(

View File

@ -426,3 +426,27 @@ def stop(context):
Stops playing.
"""
context.core.playback.stop()
@protocol.commands.add('volume', change=protocol.INT)
def volume(context, change):
"""
*musicpd.org, playback section:*
``volume {CHANGE}``
Changes volume by amount ``CHANGE``.
Note: ``volume`` is deprecated, use ``setvol`` instead.
"""
if change < -100 or change > 100:
raise exceptions.MpdArgError('Invalid volume value')
old_volume = context.core.mixer.get_volume().get()
if old_volume is None:
raise exceptions.MpdSystemError('problems setting volume')
new_volume = min(max(0, old_volume + change), 100)
success = context.core.mixer.set_volume(new_volume).get()
if not success:
raise exceptions.MpdSystemError('problems setting volume')

View File

@ -1,11 +1,15 @@
from __future__ import absolute_import, unicode_literals
import datetime
import logging
import re
from mopidy.models import TlTrack
from mopidy.mpd.protocol import tagtype_list
logger = logging.getLogger(__name__)
# TODO: special handling of local:// uri scheme
normalize_path_re = re.compile(r'[^/]+')
@ -34,8 +38,12 @@ def track_to_mpd_format(track, position=None, stream_title=None):
else:
(tlid, track) = (None, track)
if not track.uri:
logger.warning('Ignoring track without uri')
return []
result = [
('file', track.uri or ''),
('file', track.uri),
('Time', track.length and (track.length // 1000) or 0),
('Artist', concat_multi_values(track.artists, 'name')),
('Album', track.album and track.album.name or ''),
@ -164,7 +172,9 @@ def tracks_to_mpd_format(tracks, start=0, end=None):
assert len(tracks) == len(positions)
result = []
for track, position in zip(tracks, positions):
result.append(track_to_mpd_format(track, position))
formatted_track = track_to_mpd_format(track, position)
if formatted_track:
result.append(formatted_track)
return result

View File

@ -123,12 +123,14 @@ def _unwrap_stream(uri, timeout, scanner, requests_session):
logger.debug('GStreamer failed scanning URI (%s): %s', uri, exc)
scan_result = None
if scan_result is not None and not (
scan_result.mime.startswith('text/') or
scan_result.mime.startswith('application/')):
logger.debug(
'Unwrapped potential %s stream: %s', scan_result.mime, uri)
return uri
if scan_result is not None:
if scan_result.playable or (
not scan_result.mime.startswith('text/') and
not scan_result.mime.startswith('application/')
):
logger.debug(
'Unwrapped potential %s stream: %s', scan_result.mime, uri)
return uri
download_timeout = deadline - time.time()
if download_timeout < 0:

View File

@ -153,8 +153,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
self.core.library.lookup('dummy1:a', ['dummy2:a'])
def test_lookup_can_handle_uris(self):
track1 = Track(name='abc')
track2 = Track(name='def')
track1 = Track(uri='dummy1:a', name='abc')
track2 = Track(uri='dummy2:a', name='def')
self.library1.lookup().get.return_value = [track1]
self.library2.lookup().get.return_value = [track2]
@ -169,6 +169,15 @@ class CoreLibraryTest(BaseCoreLibraryTest):
self.assertFalse(self.library1.lookup.called)
self.assertFalse(self.library2.lookup.called)
def test_lookup_ignores_tracks_without_uri_set(self):
track1 = Track(uri='dummy1:a', name='abc')
track2 = Track()
self.library1.lookup().get.return_value = [track1, track2]
result = self.core.library.lookup(uris=['dummy1:a'])
self.assertEqual(result, {'dummy1:a': [track1]})
def test_refresh_with_uri_selects_dummy1_backend(self):
self.core.library.refresh('dummy1:a')

View File

@ -8,7 +8,7 @@ import pykka
from mopidy import backend, core
from mopidy.internal import deprecation
from mopidy.models import Track
from mopidy.models import TlTrack, Track
from tests import dummy_audio as audio
@ -39,21 +39,34 @@ class CorePlaybackTest(unittest.TestCase):
# A backend without the optional playback provider
self.backend3 = mock.Mock()
self.backend3.uri_schemes.get.return_value = ['dummy3']
self.backend3.has_playback().get.return_value = False
self.backend3.has_playback.return_value.get.return_value = False
# A backend for which 'change_track' fails
self.backend4 = mock.Mock()
self.backend4.uri_schemes.get.return_value = ['dummy4']
self.playback4 = mock.Mock(spec=backend.PlaybackProvider)
self.playback4.get_time_position.return_value.get.return_value = 1000
future_mock = mock.Mock(spec=pykka.future.Future)
future_mock.get.return_value = False
self.playback4.change_track.return_value = future_mock
self.backend4.playback = self.playback4
self.tracks = [
Track(uri='dummy1:a', length=40000),
Track(uri='dummy2:a', length=40000),
Track(uri='dummy3:a', length=40000), # Unplayable
Track(uri='dummy3:a', length=40000), # No playback provider
Track(uri='dummy1:b', length=40000),
Track(uri='dummy1:c', length=None), # No duration
Track(uri='dummy4:a', length=40000), # Unplayable
Track(uri='dummy1:d', length=40000),
]
self.uris = [
'dummy1:a', 'dummy2:a', 'dummy3:a', 'dummy1:b', 'dummy1:c']
'dummy1:a', 'dummy2:a', 'dummy3:a', 'dummy1:b', 'dummy1:c',
'dummy4:a', 'dummy1:d']
self.core = core.Core(config, mixer=None, backends=[
self.backend1, self.backend2, self.backend3])
self.backend1, self.backend2, self.backend3, self.backend4])
def lookup(uris):
result = {uri: [] for uri in uris}
@ -172,16 +185,40 @@ class CorePlaybackTest(unittest.TestCase):
self.playback2.change_track.return_value.get.return_value = False
self.core.tracklist.clear()
self.core.tracklist.add(uris=self.uris[:2])
self.core.tracklist.add(uris=self.uris[-2:])
tl_tracks = self.core.tracklist.tl_tracks
self.core.playback.play(tl_tracks[0])
self.core.playback.play(tl_tracks[1])
# TODO: we really want to check that the track was marked unplayable
# and that next was called. This is just an indirect way of checking
# this :(
self.assertEqual(self.core.playback.state, core.PlaybackState.STOPPED)
assert self.core.playback.get_current_tl_track() == tl_tracks[1]
def test_pause_play_skips_to_next_on_unplayable_track(self):
"""Checks that we handle backend.change_track failing."""
self.playback2.change_track.return_value.get.return_value = False
self.core.tracklist.clear()
self.core.tracklist.add(uris=self.uris[-3:])
tl_tracks = self.core.tracklist.tl_tracks
self.core.playback.pause()
self.core.playback._set_current_tl_track(tl_tracks[0])
self.core.playback.next()
self.core.playback.play(self.core.playback.get_current_tl_track())
assert self.core.playback.get_current_tl_track() == tl_tracks[2]
def test_pause_resume_skips_to_next_on_unplayable_track(self):
"""Checks that we handle backend.change_track failing."""
self.playback2.change_track.return_value.get.return_value = False
self.core.tracklist.clear()
self.core.tracklist.add(uris=self.uris[-3:])
tl_tracks = self.core.tracklist.tl_tracks
self.core.playback.pause()
self.core.playback._set_current_tl_track(tl_tracks[0])
self.core.playback.next()
self.core.playback.resume()
assert self.core.playback.get_current_tl_track() == tl_tracks[2]
@mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
@ -388,6 +425,23 @@ class CorePlaybackTest(unittest.TestCase):
self.assertNotIn(tl_track, self.core.tracklist.tl_tracks)
def test_next_in_consume_mode_removes_unplayable_track(self):
self.backend1.playback.change_track = mock.PropertyMock()
self.backend1.playback.change_track.return_value.get.return_value = (
False)
self.backend2.playback.change_track = mock.PropertyMock()
self.backend2.playback.change_track.return_value.get.return_value = (
False)
self.core.tracklist.set_consume(True)
self.core.playback.play(self.tl_tracks[0])
self.core.playback.next()
tl_tracks = self.core.tracklist.get_tl_tracks()
self.assertNotIn(self.tl_tracks[1], tl_tracks)
self.assertNotIn(self.tl_tracks[2], tl_tracks)
@mock.patch(
'mopidy.core.playback.listener.CoreListener', spec=core.CoreListener)
def test_next_emits_events(self, listener_mock):
@ -789,3 +843,102 @@ class Bug1177RegressionTest(unittest.TestCase):
c.playback.pause()
c.playback.next()
b.playback.change_track.assert_called_once_with(track2)
class Bug1352RegressionTest(unittest.TestCase):
def test(self):
config = {
'core': {
'max_tracklist_length': 10000,
}
}
b = mock.Mock()
b.uri_schemes.get.return_value = ['dummy']
b.playback = mock.Mock(spec=backend.PlaybackProvider)
b.playback.change_track.return_value.get.return_value = True
b.playback.play.return_value.get.return_value = True
track1 = Track(uri='dummy:a', length=40000)
track2 = Track(uri='dummy:b', length=40000)
tl_track2 = TlTrack(1, track2)
c = core.Core(config, mixer=None, backends=[b])
c.tracklist.add([track1, track2])
c.history._add_track = mock.PropertyMock()
c.tracklist._mark_playing = mock.PropertyMock()
c.playback.play()
b.playback.change_track.reset_mock()
c.history._add_track.reset_mock()
c.tracklist._mark_playing.reset_mock()
c.playback.pause()
c.playback.next()
b.playback.change_track.assert_called_once_with(track2)
c.history._add_track.assert_called_once_with(track2)
c.tracklist._mark_playing.assert_called_once_with(tl_track2)
class Bug1358RegressionTest(unittest.TestCase):
def setUp(self): # noqa: N802
config = {
'core': {
'max_tracklist_length': 10000,
}
}
self.backend1 = mock.Mock()
self.backend1.uri_schemes.get.return_value = ['dummy1']
self.playback1 = mock.Mock(spec=backend.PlaybackProvider)
self.backend1.playback.change_track.return_value.get.return_value = (
False)
self.backend1.playback = self.playback1
self.backend2 = mock.Mock()
self.backend2.uri_schemes.get.return_value = ['dummy2']
self.playback2 = mock.Mock(spec=backend.PlaybackProvider)
self.backend1.playback.change_track.return_value.get.return_value = (
False)
self.backend2.playback = self.playback2
self.tracks = [
Track(uri='dummy1:a', length=40000),
Track(uri='dummy2:a', length=40000),
]
self.uris = [t.uri for t in self.tracks]
self.core = core.Core(
config, mixer=None, backends=[self.backend1, self.backend2])
def lookup(uris):
result = {uri: [] for uri in uris}
for track in self.tracks:
if track.uri in result:
result[track.uri].append(track)
return result
self.lookup_patcher = mock.patch.object(self.core.library, 'lookup')
self.lookup_mock = self.lookup_patcher.start()
self.lookup_mock.side_effect = lookup
self.core.tracklist.add(uris=self.uris)
self.tl_tracks = self.core.tracklist.get_tl_tracks()
def tearDown(self): # noqa: N802
self.lookup_patcher.stop()
def test_next_in_consume_mode_removes_unplayable_track(self):
self.core.tracklist.set_consume(True)
self.core.playback.play(self.tl_tracks[0])
self.core.playback.next()
tl_tracks = self.core.tracklist.get_tl_tracks()
self.assertNotIn(self.tl_tracks[0], tl_tracks)
self.assertNotIn(self.tl_tracks[1], tl_tracks)

View File

@ -80,41 +80,6 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
self.assertTrue(self.core.tracklist.repeat.get())
self.assertInResponse('OK')
def test_setvol_below_min(self):
self.send_request('setvol "-10"')
self.assertEqual(0, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_min(self):
self.send_request('setvol "0"')
self.assertEqual(0, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_middle(self):
self.send_request('setvol "50"')
self.assertEqual(50, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_max(self):
self.send_request('setvol "100"')
self.assertEqual(100, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_above_max(self):
self.send_request('setvol "110"')
self.assertEqual(100, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_plus_is_ignored(self):
self.send_request('setvol "+10"')
self.assertEqual(10, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_without_quotes(self):
self.send_request('setvol 50')
self.assertEqual(50, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_single_off(self):
self.send_request('single "0"')
self.assertFalse(self.core.tracklist.single.get())
@ -451,9 +416,83 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK')
class PlaybackOptionsHandlerNoneMixerTest(protocol.BaseTestCase):
class VolumeTest(protocol.BaseTestCase):
def test_setvol_below_min(self):
self.send_request('setvol "-10"')
self.assertEqual(0, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_min(self):
self.send_request('setvol "0"')
self.assertEqual(0, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_middle(self):
self.send_request('setvol "50"')
self.assertEqual(50, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_max(self):
self.send_request('setvol "100"')
self.assertEqual(100, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_above_max(self):
self.send_request('setvol "110"')
self.assertEqual(100, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_plus_is_ignored(self):
self.send_request('setvol "+10"')
self.assertEqual(10, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_setvol_without_quotes(self):
self.send_request('setvol 50')
self.assertEqual(50, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_volume_plus(self):
self.core.mixer.set_volume(50)
self.send_request('volume +20')
self.assertEqual(70, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_volume_minus(self):
self.core.mixer.set_volume(50)
self.send_request('volume -20')
self.assertEqual(30, self.core.mixer.get_volume().get())
self.assertInResponse('OK')
def test_volume_less_than_minus_100(self):
self.core.mixer.set_volume(50)
self.send_request('volume -110')
self.assertEqual(50, self.core.mixer.get_volume().get())
self.assertInResponse('ACK [2@0] {volume} Invalid volume value')
def test_volume_more_than_plus_100(self):
self.core.mixer.set_volume(50)
self.send_request('volume +110')
self.assertEqual(50, self.core.mixer.get_volume().get())
self.assertInResponse('ACK [2@0] {volume} Invalid volume value')
class VolumeWithNoMixerTest(protocol.BaseTestCase):
enable_mixer = False
def test_setvol_max_error(self):
def test_setvol_without_mixer_fails(self):
self.send_request('setvol "100"')
self.assertInResponse('ACK [52@0] {setvol} problems setting volume')
def test_volume_without_mixer_failes(self):
self.send_request('volume +100')
self.assertInResponse('ACK [52@0] {volume} problems setting volume')

View File

@ -56,7 +56,7 @@ class TrackMpdFormatTest(unittest.TestCase):
def test_track_to_mpd_format_with_position_and_tlid(self):
result = translator.track_to_mpd_format(
TlTrack(2, Track()), position=1)
TlTrack(2, Track(uri='a uri')), position=1)
self.assertIn(('Pos', 1), result)
self.assertIn(('Id', 2), result)
@ -153,13 +153,17 @@ class PlaylistMpdFormatTest(unittest.TestCase):
def test_mpd_format(self):
playlist = Playlist(tracks=[
Track(track_no=1), Track(track_no=2), Track(track_no=3)])
Track(uri='foo', track_no=1),
Track(uri='bar', track_no=2),
Track(uri='baz', track_no=3)])
result = translator.playlist_to_mpd_format(playlist)
self.assertEqual(len(result), 3)
def test_mpd_format_with_range(self):
playlist = Playlist(tracks=[
Track(track_no=1), Track(track_no=2), Track(track_no=3)])
Track(uri='foo', track_no=1),
Track(uri='bar', track_no=2),
Track(uri='baz', track_no=3)])
result = translator.playlist_to_mpd_format(playlist, 1, 2)
self.assertEqual(len(result), 1)
self.assertEqual(dict(result[0])['Track'], 2)

View File

@ -38,7 +38,9 @@ def audio():
@pytest.fixture
def scanner():
return mock.Mock(spec=scan.Scanner)
scan_mock = mock.Mock(spec=scan.Scanner)
scan_mock.scan.return_value = None
return scan_mock
@pytest.fixture
@ -58,7 +60,24 @@ class TestTranslateURI(object):
@responses.activate
def test_audio_stream_returns_same_uri(self, scanner, provider):
scanner.scan.return_value.mime = 'audio/mpeg'
scanner.scan.side_effect = [
# Set playable to False to test detection by mimetype
mock.Mock(mime='audio/mpeg', playable=False),
]
result = provider.translate_uri(STREAM_URI)
scanner.scan.assert_called_once_with(STREAM_URI, timeout=mock.ANY)
assert result == STREAM_URI
@responses.activate
def test_playable_ogg_stream_is_not_considered_a_playlist(
self, scanner, provider):
scanner.scan.side_effect = [
# Set playable to True to ignore detection as possible playlist
mock.Mock(mime='application/ogg', playable=True),
]
result = provider.translate_uri(STREAM_URI)
@ -70,8 +89,10 @@ class TestTranslateURI(object):
self, scanner, provider, caplog):
scanner.scan.side_effect = [
mock.Mock(mime='text/foo'), # scanning playlist
mock.Mock(mime='audio/mpeg'), # scanning stream
# Scanning playlist
mock.Mock(mime='text/foo', playable=False),
# Scanning stream
mock.Mock(mime='audio/mpeg', playable=True),
]
responses.add(
responses.GET, PLAYLIST_URI,
@ -100,8 +121,10 @@ class TestTranslateURI(object):
@responses.activate
def test_xml_playlist_with_mpeg_stream(self, scanner, provider):
scanner.scan.side_effect = [
mock.Mock(mime='application/xspf+xml'), # scanning playlist
mock.Mock(mime='audio/mpeg'), # scanning stream
# Scanning playlist
mock.Mock(mime='application/xspf+xml', playable=False),
# Scanning stream
mock.Mock(mime='audio/mpeg', playable=True),
]
responses.add(
responses.GET, PLAYLIST_URI,
@ -120,8 +143,10 @@ class TestTranslateURI(object):
self, scanner, provider, caplog):
scanner.scan.side_effect = [
exceptions.ScannerError('some failure'), # scanning playlist
mock.Mock(mime='audio/mpeg'), # scanning stream
# Scanning playlist
exceptions.ScannerError('some failure'),
# Scanning stream
mock.Mock(mime='audio/mpeg', playable=True),
]
responses.add(
responses.GET, PLAYLIST_URI,
@ -169,7 +194,9 @@ class TestTranslateURI(object):
@responses.activate
def test_playlist_references_itself(self, scanner, provider, caplog):
scanner.scan.return_value.mime = 'text/foo'
scanner.scan.side_effect = [
mock.Mock(mime='text/foo', playable=False)
]
responses.add(
responses.GET, PLAYLIST_URI,
body=BODY.replace(STREAM_URI, PLAYLIST_URI),

View File

@ -9,6 +9,7 @@ from mopidy import httpclient
@pytest.mark.parametrize("config,expected", [
({}, None),
({'hostname': ''}, None),
({'hostname': 'proxy.lan'}, 'http://proxy.lan:80'),
({'scheme': None, 'hostname': 'proxy.lan'}, 'http://proxy.lan:80'),
({'scheme': 'https', 'hostname': 'proxy.lan'}, 'https://proxy.lan:80'),
@ -16,6 +17,8 @@ from mopidy import httpclient
({'password': 'pass', 'hostname': 'proxy.lan'}, 'http://proxy.lan:80'),
({'hostname': 'proxy.lan', 'port': 8080}, 'http://proxy.lan:8080'),
({'hostname': 'proxy.lan', 'port': -1}, 'http://proxy.lan:80'),
({'hostname': 'proxy.lan', 'port': None}, 'http://proxy.lan:80'),
({'hostname': 'proxy.lan', 'port': ''}, 'http://proxy.lan:80'),
({'username': 'user', 'password': 'pass', 'hostname': 'proxy.lan'},
'http://user:pass@proxy.lan:80'),
])

View File

@ -8,62 +8,5 @@ from mopidy import __version__
class VersionTest(unittest.TestCase):
def assertVersionLess(self, first, second): # noqa: N802
self.assertLess(StrictVersion(first), StrictVersion(second))
def test_current_version_is_parsable_as_a_strict_version_number(self):
StrictVersion(__version__)
def test_versions_can_be_strictly_ordered(self):
self.assertVersionLess('0.1.0a0', '0.1.0a1')
self.assertVersionLess('0.1.0a1', '0.1.0a2')
self.assertVersionLess('0.1.0a2', '0.1.0a3')
self.assertVersionLess('0.1.0a3', '0.1.0')
self.assertVersionLess('0.1.0', '0.2.0')
self.assertVersionLess('0.1.0', '1.0.0')
self.assertVersionLess('0.2.0', '0.3.0')
self.assertVersionLess('0.3.0', '0.3.1')
self.assertVersionLess('0.3.1', '0.4.0')
self.assertVersionLess('0.4.0', '0.4.1')
self.assertVersionLess('0.4.1', '0.5.0')
self.assertVersionLess('0.5.0', '0.6.0')
self.assertVersionLess('0.6.0', '0.6.1')
self.assertVersionLess('0.6.1', '0.7.0')
self.assertVersionLess('0.7.0', '0.7.1')
self.assertVersionLess('0.7.1', '0.7.2')
self.assertVersionLess('0.7.2', '0.7.3')
self.assertVersionLess('0.7.3', '0.8.0')
self.assertVersionLess('0.8.0', '0.8.1')
self.assertVersionLess('0.8.1', '0.9.0')
self.assertVersionLess('0.9.0', '0.10.0')
self.assertVersionLess('0.10.0', '0.11.0')
self.assertVersionLess('0.11.0', '0.11.1')
self.assertVersionLess('0.11.1', '0.12.0')
self.assertVersionLess('0.12.0', '0.13.0')
self.assertVersionLess('0.13.0', '0.14.0')
self.assertVersionLess('0.14.0', '0.14.1')
self.assertVersionLess('0.14.1', '0.14.2')
self.assertVersionLess('0.14.2', '0.15.0')
self.assertVersionLess('0.15.0', '0.16.0')
self.assertVersionLess('0.16.0', '0.17.0')
self.assertVersionLess('0.17.0', '0.18.0')
self.assertVersionLess('0.18.0', '0.18.1')
self.assertVersionLess('0.18.1', '0.18.2')
self.assertVersionLess('0.18.2', '0.18.3')
self.assertVersionLess('0.18.3', '0.19.0')
self.assertVersionLess('0.19.0', '0.19.1')
self.assertVersionLess('0.19.1', '0.19.2')
self.assertVersionLess('0.19.2', '0.19.3')
self.assertVersionLess('0.19.3', '0.19.4')
self.assertVersionLess('0.19.4', '0.19.5')
self.assertVersionLess('0.19.5', '1.0.0')
self.assertVersionLess('1.0.0', '1.0.1')
self.assertVersionLess('1.0.1', '1.0.2')
self.assertVersionLess('1.0.2', '1.0.3')
self.assertVersionLess('1.0.3', '1.0.4')
self.assertVersionLess('1.0.4', '1.0.5')
self.assertVersionLess('1.0.5', '1.0.6')
self.assertVersionLess('1.0.6', '1.0.7')
self.assertVersionLess('1.0.7', '1.0.8')
self.assertVersionLess('1.0.8', __version__)
self.assertVersionLess(__version__, '1.1.1')