Merge branch 'develop' of https://github.com/mopidy/mopidy into fix/310-persist-mopidy-state-between-runs

Conflicts:
	mopidy/audio/scan.py
Fix conflicts.
This commit is contained in:
Jens Luetjen 2016-02-03 22:01:51 +01:00
commit 0a1e43c876
57 changed files with 1433 additions and 1291 deletions

View File

@ -16,7 +16,7 @@ env:
before_install: before_install:
- "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573 - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround tornadoweb/tornado#1573
- "sudo apt-get update -qq" - "sudo apt-get update -qq"
- "sudo apt-get install -y graphviz-dev gstreamer0.10-plugins-good python-gst0.10" - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad python-gst-1.0"
install: install:
- "pip install tox" - "pip install tox"

View File

@ -76,3 +76,4 @@
- Jelle van der Waa <jelle@vdwaa.nl> - Jelle van der Waa <jelle@vdwaa.nl>
- Alex Malone <jalexmalone@gmail.com> - Alex Malone <jalexmalone@gmail.com>
- Daniel Hahler <git@thequod.de> - Daniel Hahler <git@thequod.de>
- Bryan Bennett <bbenne10@gmail.com>

View File

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

View File

@ -4,7 +4,7 @@
Authors 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 licensed under the `Apache License, Version 2.0
<http://www.apache.org/licenses/LICENSE-2.0>`_. <http://www.apache.org/licenses/LICENSE-2.0>`_.

View File

@ -10,6 +10,12 @@ v1.2.0 (UNRELEASED)
Feature release. Feature release.
Dependencies
------------
- Mopidy now requires GStreamer 1.x, as we've finally ported from GStreamer
0.10.
Core API Core API
-------- --------
@ -126,6 +132,33 @@ Cleanups
- Catch errors when loading :confval:`logging/config_file`. - Catch errors when loading :confval:`logging/config_file`.
(Fixes: :issue:`1320`) (Fixes: :issue:`1320`)
Audio
-----
- **Breaking:** The audio scanner now returns ISO-8601 formatted strings
instead of :class:`~datetime.datetime` objects for dates found in tags.
Because of this change, we can now return years without months or days, which
matches the semantics of the date fields in our data models.
- **Breaking:** :meth:`mopidy.audio.Audio.set_appsrc`'s ``caps`` argument has
changed format due to the upgrade from GStreamer 0.10 to GStreamer 1. As
far as we know, this is only used by Mopidy-Spotify. As an example, with
GStreamer 0.10 the Mopidy-Spotify caps was::
audio/x-raw-int, endianness=(int)1234, channels=(int)2, width=(int)16,
depth=(int)16, signed=(boolean)true, rate=(int)44100
With GStreamer 1 this changes to::
audio/x-raw,format=S16LE,rate=44100,channels=2,layout=interleaved
If your Mopidy backend uses ``set_appsrc()``, please refer to GStreamer
documentation for details on the new caps string format.
- **Deprecated:** :func:`mopidy.audio.utils.create_buffer`'s ``capabilities``
argument is no longer in use and will be removed in the future. As far as we
know, this is only used by Mopidy-Spotify.
Gapless Gapless
------- -------
@ -144,11 +177,28 @@ Gapless
cases. (Fixes: :issue:`1305` PR: :issue:`1346`) cases. (Fixes: :issue:`1305` PR: :issue:`1346`)
v1.1.2 (UNRELEASED) v1.1.2 (2016-01-18)
=================== ===================
Bug fix release. 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 - 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 for playlist parsing. Just looking at MIME type prefixes isn't enough, as for
example Ogg Vorbis has the MIME type ``application/ogg``. (Fixes: example Ogg Vorbis has the MIME type ``application/ogg``. (Fixes:
@ -157,6 +207,18 @@ Bug fix release.
- Local: If the scan or clear commands are used on a library that does not - Local: If the scan or clear commands are used on a library that does not
exist, exit with an error. (Fixes: :issue:`1298`) 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) v1.1.1 (2015-09-14)
=================== ===================

View File

@ -167,5 +167,5 @@ projects are a real match made in heaven."
Partify Partify
------- -------
`Partify <https://github.com/fhats/partify>`_ is a web based MPD client focussing on `Partify <https://github.com/fhats/partify>`_ is a web based MPD client
making music playing collaborative and social. focussing on making music playing collaborative and social.

View File

@ -93,14 +93,14 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
project = 'Mopidy' 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 from mopidy.internal.versioning import get_version
release = get_version() release = get_version()
version = '.'.join(release.split('.')[:2]) version = '.'.join(release.split('.')[:2])
# To make the build reproducible, avoid using today's date in the manpages # To make the build reproducible, avoid using today's date in the manpages
today = '2015' today = '2016'
exclude_trees = ['_build'] exclude_trees = ['_build']

View File

@ -25,6 +25,10 @@ create the configuration file yourself, or run the ``mopidy`` command, and it
will create an empty config file for you and print what config values must be will create an empty config file for you and print what config values must be
set to successfully start Mopidy. set to successfully start Mopidy.
If running Mopidy as a service, the location of the config file and other
details documented here differs a bit. See :ref:`service` for details about
this.
When you have created the configuration file, open it in a text editor, and add When you have created the configuration file, open it in a text editor, and add
the config values you want to change. If you want to keep the default for a the config values you want to change. If you want to keep the default for a
config value, you **should not** add it to the config file, but leave it out so config value, you **should not** add it to the config file, but leave it out so

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

@ -115,7 +115,7 @@ Bundled with Mopidy. See :ref:`ext-local`.
Mopidy-Local-Images Mopidy-Local-Images
=================== ===================
https://github.com/tkem/mopidy-local-images https://github.com/mopidy/mopidy-local-images
Extension which plugs into Mopidy-Local to allow Web clients access to Extension which plugs into Mopidy-Local to allow Web clients access to
album art embedded in local media files. Not to be used on its own, album art embedded in local media files. Not to be used on its own,
@ -126,7 +126,7 @@ local library provider being used.
Mopidy-Local-SQLite Mopidy-Local-SQLite
=================== ===================
https://github.com/tkem/mopidy-local-sqlite https://github.com/mopidy/mopidy-local-sqlite
Extension which plugs into Mopidy-Local to use an SQLite database to keep Extension which plugs into Mopidy-Local to use an SQLite database to keep
track of your local media. This extension lets you browse your music collection track of your local media. This extension lets you browse your music collection

BIN
docs/ext/spotmop.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -164,6 +164,22 @@ To install, run::
pip install Mopidy-Simple-Webclient pip install Mopidy-Simple-Webclient
Mopidy-Spotmop
==============
https://github.com/jaedb/spotmop
A client targeted at Spotify users. Made by James Barnsley.
.. image:: /ext/spotmop.jpg
:width: 720
:height: 455
To install, run::
pip install Mopidy-Spotmop
Mopidy-WebSettings Mopidy-WebSettings
================== ==================

View File

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

View File

@ -16,7 +16,8 @@ If you are running Arch Linux, you can install Mopidy using the
pacman -Syu pacman -Syu
#. Finally, you need to set a couple of :doc:`config values </config>`, and #. 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 Installing extensions

View File

@ -48,12 +48,9 @@ and armhf (compatible with Raspberry Pi 1 and 2).
sudo apt-get update sudo apt-get update
sudo apt-get install mopidy 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 #. 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 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:: 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 For a full list of available Mopidy extensions, including those not
installable from apt.mopidy.com, see :ref:`ext`. 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`. from Homebrew, see :ref:`ext`.
.. _osx-service:
Running Mopidy automatically on login Running Mopidy automatically on login
===================================== =====================================

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,75 +1,68 @@
.. _raspberrypi-installation: .. _raspberrypi-installation:
************************************* ************
Raspberry Pi: Mopidy on a credit card Raspberry Pi
************************************* ************
Mopidy runs nicely on a `Raspberry Pi <https://www.raspberrypi.org/>`_. As of Mopidy runs on all versions of `Raspberry Pi <https://www.raspberrypi.org/>`_.
January 2013, Mopidy will run with Spotify support on both the armel However, note that Raspberry Pi 2 B's CPU is approximately six times as
(soft-float) and armhf (hard-float) architectures, which includes the Raspbian powerful as Raspberry Pi 1 and Raspberry Pi Zero, so Mopidy will be more joyful
distribution. to use on a Raspberry Pi 2.
.. image:: raspberry-pi-by-jwrodgers.jpg .. image:: raspberrypi2.jpg
:width: 640 :width: 640
:height: 427 :height: 363
.. _raspi-wheezy: .. _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 If you're only using your Pi for Mopidy, go with Jessie Lite as you won't
- Debian "wheezy" for armel (soft-float) 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 #. Flash the Raspbian image you downloaded to your SD card.
you a lot better performance.
#. Download the latest "wheezy" disk image from See the `Raspberry Pi installation docs
https://www.raspberrypi.org/downloads/. This was last tested with the images <https://www.raspberrypi.org/documentation/installation/installing-images/README.md>`_
from 2013-05-25 for armhf and 2013-05-29 for armel. for instructions.
#. Flash the OS image to your SD card. See #. If you connect a monitor and a keyboard, you'll see that the Pi boots right
http://elinux.org/RPi_Easy_SD_Card_Setup for help. into the ``raspi-config`` tool.
#. If you have an SD card that's >2 GB, you don't have to resize the file If you boot with only a network cable connected, you'll have to find the IP
systems on another computer. Just boot up your Raspberry Pi with the address of the Pi yourself, e.g. by looking in the client list on your
unaltered partions, and it will boot right into the ``raspi-config`` tool, router/DHCP server. When you have found the Pi's IP address, you can SSH to
which will let you grow the root file system to fill the SD card. This tool the IP address and login with the user ``pi`` and password ``raspberry``.
will also allow you do other useful stuff, like turning on the SSH server. 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 #. Use the ``raspi-config`` tool to setup the basics of your Pi. You might want
``raspberry``. To become root, just enter ``sudo -i``. 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 Once done, select "Finish" and restart your Pi.
loaded on boot::
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 #. Once you've rebooted and has logged in as the ``pi`` user, you can enter
connector, I have to run:: ``sudo -i`` to become ``root``.
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.
#. Install Mopidy and its dependencies as described in :ref:`debian-install`. #. Install Mopidy and its dependencies as described in :ref:`debian-install`.
@ -79,114 +72,19 @@ you a lot better performance.
starting at boot. starting at boot.
Appendix A: Fixing audio quality issues Testing sound output
======================================= ====================
As of about April 2013 the following steps should resolve any audio You can test sound output independent of Mopidy by running::
issues for HDMI and analog without the use of an external USB sound
card.
#. Ensure your system is up to date. On Debian based systems run:: aplay /usr/share/sounds/alsa/Front_Center.wav
sudo apt-get update If you hear a voice saying "Front Center", then your sound is working.
sudo apt-get dist-upgrade
#. Ensure you have a new enough firmware. On Debian based systems If you want to change your audio output setting, simply rerun ``sudo
`rpi-update <https://github.com/Hexxeh/rpi-update>`_ raspi-config``. Alternatively, you can change the audio output setting
can be used. directly by running:
#. Update either ``~/.asoundrc`` or ``/etc/asound.conf`` to the - Auto (HDMI if connected, else 3.5mm jack): ``sudo amixer cset numid=3 0``
following:: - Use 3.5mm jack: ``sudo amixer cset numid=3 1``
- Use HDMI: ``sudo amixer cset numid=3 2``
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 issue
<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

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -37,36 +37,40 @@ please follow the directions :ref:`here <contributing>`.
On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the On Fedora Linux, you must replace ``pip`` with ``pip-python`` in the
following steps. following steps.
#. Then you'll need to install GStreamer 0.10 (>= 0.10.31, < 0.11), with Python #. Then you'll need to install GStreamer 1.x (>= 1.2), with Python bindings.
bindings. GStreamer is packaged for most popular Linux distributions. Search GStreamer is packaged for most popular Linux distributions. Search for
for GStreamer in your package manager, and make sure to install the Python GStreamer in your package manager, and make sure to install the Python
bindings, and the "good" and "ugly" plugin sets. bindings, and the "good" and "ugly" plugin sets.
If you use Debian/Ubuntu you can install GStreamer like this:: If you use Debian/Ubuntu you can install GStreamer like this::
sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ sudo apt-get install python-gst-1.0 gstreamer1.0-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools gstreamer1.0-plugins-ugly gstreamer1.0-tools
If you use Arch Linux, install the following packages from the official If you use Arch Linux, install the following packages from the official
repository:: repository::
sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \ sudo pacman -S python2-gobject gst-python gst-plugins-good
gstreamer0.10-ugly-plugins gst-plugins-ugly
.. warning::
``gst-python`` installs GStreamer GI overrides for Python 3. As far as
we know, Arch currently lacks a package with the corresponding overrides
built for Python 2. If a ``gst-python2`` package is added, it will
depend on ``python2-gobject``, so we can then shorten this package list.
If you use Fedora you can install GStreamer like this:: If you use Fedora you can install GStreamer like this::
sudo yum install -y python-gst0.10 gstreamer0.10-plugins-good \ sudo yum install -y python-gstreamer1 gstreamer1-plugins-good \
gstreamer0.10-plugins-ugly gstreamer0.10-tools gstreamer1-plugins-ugly
If you use Gentoo you need to be careful because GStreamer 0.10 is in a If you use Gentoo you can install GStreamer like this::
different lower slot than 1.0, the default. Your emerge commands will need
to include the slot::
emerge -av gst-python gst-plugins-bad:0.10 gst-plugins-good:0.10 \ emerge -av gst-python gst-plugins-meta
gst-plugins-ugly:0.10 gst-plugins-meta:0.10
``gst-plugins-meta:0.10`` is the one that actually pulls in the plugins you ``gst-plugins-meta`` is the one that actually pulls in the plugins you want,
want, so pay attention to the use flags, e.g. ``alsa``, ``mp3``, etc. so pay attention to the USE flags, e.g. ``alsa``, ``mp3``, etc.
#. Install the latest release of Mopidy:: #. Install the latest release of Mopidy::
@ -76,11 +80,6 @@ please follow the directions :ref:`here <contributing>`.
<https://pypi.python.org/pypi/Mopidy>`_. To upgrade Mopidy to future <https://pypi.python.org/pypi/Mopidy>`_. To upgrade Mopidy to future
releases, just rerun this command. releases, just rerun this command.
Alternatively, if you want to track Mopidy development closer, you may
install a snapshot of Mopidy's ``develop`` Git branch using pip::
sudo pip install --allow-unverified=mopidy mopidy==dev
#. Finally, you need to set a couple of :doc:`config values </config>`, and #. 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>`.

View File

@ -39,17 +39,8 @@ using ``pkill``::
pkill mopidy pkill mopidy
Init scripts Running as a service
============ ====================
- The ``mopidy`` package at `apt.mopidy.com <http://apt.mopidy.com/>`__ comes Once you're done exploring Mopidy and want to run it as a proper service, check
with an `sysvinit init script out :ref:`service`.
<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.
- Issue :issue:`266` contains a bunch of init scripts for Mopidy, including
Upstart init scripts.

137
docs/service.rst Normal file
View File

@ -0,0 +1,137 @@
.. _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/mopidy.conf`, not in your user's
home directory.
mopidy user
===========
The Mopidy service runs 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`.
Configure PulseAudio
====================
When using PulseAudio, you will typically have a PulseAudio server run by your
main user. Since Mopidy is running as its own user, it can't access this server
directly. Running PulseAudio as a system-wide daemon is discouraged by upstream
(see `here
<http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/WhatIsWrongWithSystemWide/>`_
for details). Rather you can configure PulseAudio and Mopidy so Mopidy sends
the sound to the PulseAudio server already running as your main user.
First, configure PulseAudio to accept sound over TCP from localhost by
uncommenting or adding the TCP module to :file:`/etc/pulse/default.pa` or
:file:`$XDG_CONFIG_HOME/pulse/default.pa` (typically
:file:`~/.config/pulse/default.pa`)::
### Network access (may be configured with paprefs, so leave this commented
### here if you plan to use paprefs)
#load-module module-esound-protocol-tcp
load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1
#load-module module-zeroconf-publish
Next, configure Mopidy to use this PulseAudio server::
[audio]
output = pulsesink server=127.0.0.1
After this, restart both PulseAudio and Mopidy::
pulseaudio --kill
start-pulseaudio-x11
sudo systemctl restart mopidy
If you are not running any X server, run ``pulseaudio --start`` instead of
``start-pulseaudio-x11``.
If you don't want to hard code the output in your Mopidy config, you can
instead of adding any config to Mopidy add this to
:file:`~mopidy/.pulse/client.conf`::
default-server=127.0.0.1

View File

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

View File

@ -4,24 +4,8 @@ import logging
import os import os
import signal import signal
import sys import sys
import textwrap
try: from mopidy.internal.gi import Gst # noqa: Import to initialize
import gobject # noqa
except ImportError:
print(textwrap.dedent("""
ERROR: The gobject Python package was not found.
Mopidy requires GStreamer (and GObject) to work. These are C libraries
with a number of dependencies themselves, and cannot be installed with
the regular Python tools like pip.
Please see http://docs.mopidy.com/en/latest/installation/ for
instructions on how to install the required dependencies.
"""))
raise
gobject.threads_init()
try: try:
# Make GObject's mainloop the event loop for python-dbus # Make GObject's mainloop the event loop for python-dbus
@ -33,13 +17,6 @@ except ImportError:
import pykka.debug import pykka.debug
# Extract any command line arguments. This needs to be done before GStreamer is
# imported, so that GStreamer doesn't hijack e.g. ``--help``.
mopidy_args = sys.argv[1:]
sys.argv[1:] = []
from mopidy import commands, config as config_lib, ext from mopidy import commands, config as config_lib, ext
from mopidy.internal import encoding, log, path, process, versioning from mopidy.internal import encoding, log, path, process, versioning
@ -73,7 +50,7 @@ def main():
data.command.set(extension=data.extension) data.command.set(extension=data.extension)
root_cmd.add_child(data.extension.ext_name, data.command) root_cmd.add_child(data.extension.ext_name, data.command)
args = root_cmd.parse(mopidy_args) args = root_cmd.parse(sys.argv[1:])
config, config_errors = config_lib.load( config, config_errors = config_lib.load(
args.config_files, args.config_files,

View File

@ -4,65 +4,28 @@ import logging
import os import os
import threading import threading
import gobject
import pygst
pygst.require('0.10')
import gst # noqa
import gst.pbutils # noqa
import pykka import pykka
from mopidy import exceptions from mopidy import exceptions
from mopidy.audio import icy, utils from mopidy.audio import tags as tags_lib, utils
from mopidy.audio.constants import PlaybackState from mopidy.audio.constants import PlaybackState
from mopidy.audio.listener import AudioListener from mopidy.audio.listener import AudioListener
from mopidy.internal import deprecation, process from mopidy.internal import deprecation, process
from mopidy.internal.gi import GObject, Gst, GstPbutils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# This logger is only meant for debug logging of low level gstreamer info such # This logger is only meant for debug logging of low level GStreamer info such
# as callbacks, event, messages and direct interaction with GStreamer such as # as callbacks, event, messages and direct interaction with GStreamer such as
# set_state on a pipeline. # set_state() on a pipeline.
gst_logger = logging.getLogger('mopidy.audio.gst') gst_logger = logging.getLogger('mopidy.audio.gst')
icy.register()
_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,
gst.STATE_NULL: PlaybackState.STOPPED} Gst.State.NULL: PlaybackState.STOPPED,
}
class _Signals(object):
"""Helper for tracking gobject signal registrations"""
def __init__(self):
self._ids = {}
def connect(self, element, event, func, *args):
"""Connect a function + args to signal event on an element.
Each event may only be handled by one callback in this implementation.
"""
assert (element, event) not in self._ids
self._ids[(element, event)] = element.connect(event, func, *args)
def disconnect(self, element, event):
"""Disconnect whatever handler we have for and element+event pair.
Does nothing it the handler has already been removed.
"""
signal_id = self._ids.pop((element, event), None)
if signal_id is not None:
element.disconnect(signal_id)
def clear(self):
"""Clear all registered signal handlers."""
for element, event in self._ids.keys():
element.disconnect(self._ids.pop((element, event)))
# TODO: expose this as a property on audio? # TODO: expose this as a property on audio?
@ -71,7 +34,7 @@ class _Appsrc(object):
"""Helper class for dealing with appsrc based playback.""" """Helper class for dealing with appsrc based playback."""
def __init__(self): def __init__(self):
self._signals = _Signals() self._signals = utils.Signals()
self.reset() self.reset()
def reset(self): def reset(self):
@ -120,9 +83,11 @@ class _Appsrc(object):
if buffer_ is None: if buffer_ is None:
gst_logger.debug('Sending appsrc end-of-stream event.') gst_logger.debug('Sending appsrc end-of-stream event.')
return self._source.emit('end-of-stream') == gst.FLOW_OK result = self._source.emit('end-of-stream')
return result == Gst.FlowReturn.OK
else: else:
return self._source.emit('push-buffer', buffer_) == gst.FLOW_OK result = self._source.emit('push-buffer', buffer_)
return result == Gst.FlowReturn.OK
def _on_signal(self, element, clocktime, func): def _on_signal(self, element, clocktime, func):
# This shim is used to ensure we always return true, and also handles # This shim is used to ensure we always return true, and also handles
@ -135,29 +100,30 @@ class _Appsrc(object):
# TODO: expose this as a property on audio when #790 gets further along. # TODO: expose this as a property on audio when #790 gets further along.
class _Outputs(gst.Bin): class _Outputs(Gst.Bin):
def __init__(self): def __init__(self):
gst.Bin.__init__(self, 'outputs') Gst.Bin.__init__(self)
# TODO gst1: Set 'outputs' as the Bin name for easier debugging
self._tee = gst.element_factory_make('tee') self._tee = Gst.ElementFactory.make('tee')
self.add(self._tee) self.add(self._tee)
ghost_pad = gst.GhostPad('sink', self._tee.get_pad('sink')) ghost_pad = Gst.GhostPad.new('sink', self._tee.get_static_pad('sink'))
self.add_pad(ghost_pad) self.add_pad(ghost_pad)
# Add an always connected fakesink which respects the clock so the tee # Add an always connected fakesink which respects the clock so the tee
# doesn't fail even if we don't have any outputs. # doesn't fail even if we don't have any outputs.
fakesink = gst.element_factory_make('fakesink') fakesink = Gst.ElementFactory.make('fakesink')
fakesink.set_property('sync', True) fakesink.set_property('sync', True)
self._add(fakesink) self._add(fakesink)
def add_output(self, description): def add_output(self, description):
# XXX This only works for pipelines not in use until #790 gets done. # XXX This only works for pipelines not in use until #790 gets done.
try: try:
output = gst.parse_bin_from_description( output = Gst.parse_bin_from_description(
description, ghost_unconnected_pads=True) description, ghost_unlinked_pads=True)
except gobject.GError as ex: except GObject.GError as ex:
logger.error( logger.error(
'Failed to create audio output "%s": %s', description, ex) 'Failed to create audio output "%s": %s', description, ex)
raise exceptions.AudioException(bytes(ex)) raise exceptions.AudioException(bytes(ex))
@ -166,7 +132,7 @@ class _Outputs(gst.Bin):
logger.info('Audio output set to "%s"', description) logger.info('Audio output set to "%s"', description)
def _add(self, element): def _add(self, element):
queue = gst.element_factory_make('queue') queue = Gst.ElementFactory.make('queue')
self.add(element) self.add(element)
self.add(queue) self.add(queue)
queue.link(element) queue.link(element)
@ -181,7 +147,7 @@ class SoftwareMixer(object):
self._element = None self._element = None
self._last_volume = None self._last_volume = None
self._last_mute = None self._last_mute = None
self._signals = _Signals() self._signals = utils.Signals()
def setup(self, element, mixer_ref): def setup(self, element, mixer_ref):
self._element = element self._element = element
@ -223,7 +189,8 @@ class _Handler(object):
def setup_event_handling(self, pad): def setup_event_handling(self, pad):
self._pad = pad self._pad = pad
self._event_handler_id = pad.add_event_probe(self.on_event) self._event_handler_id = pad.add_probe(
Gst.PadProbeType.EVENT_BOTH, self.on_pad_event)
def teardown_message_handling(self): def teardown_message_handling(self):
bus = self._element.get_bus() bus = self._element.get_bus()
@ -232,55 +199,59 @@ class _Handler(object):
self._message_handler_id = None self._message_handler_id = None
def teardown_event_handling(self): def teardown_event_handling(self):
self._pad.remove_event_probe(self._event_handler_id) self._pad.remove_probe(self._event_handler_id)
self._event_handler_id = None self._event_handler_id = None
def on_message(self, bus, msg): def on_message(self, bus, msg):
if msg.type == gst.MESSAGE_STATE_CHANGED and msg.src == self._element: if msg.type == Gst.MessageType.STATE_CHANGED:
self.on_playbin_state_changed(*msg.parse_state_changed()) if msg.src != self._element:
elif msg.type == gst.MESSAGE_BUFFERING: return
self.on_buffering(msg.parse_buffering(), msg.structure) old_state, new_state, pending_state = msg.parse_state_changed()
elif msg.type == gst.MESSAGE_EOS: self.on_playbin_state_changed(old_state, new_state, pending_state)
elif msg.type == Gst.MessageType.BUFFERING:
self.on_buffering(msg.parse_buffering(), msg.get_structure())
elif msg.type == Gst.MessageType.EOS:
self.on_end_of_stream() self.on_end_of_stream()
elif msg.type == gst.MESSAGE_ERROR: elif msg.type == Gst.MessageType.ERROR:
self.on_error(*msg.parse_error()) error, debug = msg.parse_error()
elif msg.type == gst.MESSAGE_WARNING: self.on_error(error, debug)
self.on_warning(*msg.parse_warning()) elif msg.type == Gst.MessageType.WARNING:
elif msg.type == gst.MESSAGE_ASYNC_DONE: error, debug = msg.parse_warning()
self.on_warning(error, debug)
elif msg.type == Gst.MessageType.ASYNC_DONE:
self.on_async_done() self.on_async_done()
elif msg.type == gst.MESSAGE_TAG: elif msg.type == Gst.MessageType.TAG:
self.on_tag(msg.parse_tag()) taglist = msg.parse_tag()
elif msg.type == gst.MESSAGE_ELEMENT: self.on_tag(taglist)
if gst.pbutils.is_missing_plugin_message(msg): elif msg.type == Gst.MessageType.ELEMENT:
if GstPbutils.is_missing_plugin_message(msg):
self.on_missing_plugin(msg) self.on_missing_plugin(msg)
elif msg.type == Gst.MessageType.STREAM_START:
self.on_stream_start()
def on_event(self, pad, event): def on_pad_event(self, pad, pad_probe_info):
if event.type == gst.EVENT_NEWSEGMENT: event = pad_probe_info.get_event()
self.on_new_segment(*event.parse_new_segment()) if event.type == Gst.EventType.SEGMENT:
elif event.type == gst.EVENT_SINK_MESSAGE: self.on_segment(event.parse_segment())
# Handle stream changed messages when they reach our output bin. return Gst.PadProbeReturn.OK
# If we listen for it on the bus we get one per tee branch.
msg = event.parse_sink_message()
if msg.structure.has_name('playbin2-stream-changed'):
self.on_stream_changed(msg.structure['uri'])
return True
def on_playbin_state_changed(self, old_state, new_state, pending_state): def on_playbin_state_changed(self, old_state, new_state, pending_state):
gst_logger.debug('Got state-changed message: old=%s new=%s pending=%s', gst_logger.debug(
'Got STATE_CHANGED bus message: old=%s new=%s pending=%s',
old_state.value_name, new_state.value_name, old_state.value_name, new_state.value_name,
pending_state.value_name) pending_state.value_name)
if new_state == gst.STATE_READY and pending_state == gst.STATE_NULL: if new_state == Gst.State.READY and pending_state == Gst.State.NULL:
# XXX: We're not called on the last state change when going down to # XXX: We're not called on the last state change when going down to
# NULL, so we rewrite the second to last call to get the expected # NULL, so we rewrite the second to last call to get the expected
# behavior. # behavior.
new_state = gst.STATE_NULL new_state = Gst.State.NULL
pending_state = gst.STATE_VOID_PENDING pending_state = Gst.State.VOID_PENDING
if pending_state != gst.STATE_VOID_PENDING: if pending_state != Gst.State.VOID_PENDING:
return # Ignore intermediate state changes return # Ignore intermediate state changes
if new_state == gst.STATE_READY: if new_state == Gst.State.READY:
return # Ignore READY state as it's GStreamer specific return # Ignore READY state as it's GStreamer specific
new_state = _GST_STATE_MAPPING[new_state] new_state = _GST_STATE_MAPPING[new_state]
@ -299,80 +270,96 @@ class _Handler(object):
AudioListener.send('stream_changed', uri=None) AudioListener.send('stream_changed', uri=None)
if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ: if 'GST_DEBUG_DUMP_DOT_DIR' in os.environ:
gst.DEBUG_BIN_TO_DOT_FILE( Gst.debug_bin_to_dot_file(
self._audio._playbin, gst.DEBUG_GRAPH_SHOW_ALL, 'mopidy') self._audio._playbin, Gst.DebugGraphDetails.ALL, 'mopidy')
def on_buffering(self, percent, structure=None): def on_buffering(self, percent, structure=None):
if structure and structure.has_field('buffering-mode'): if structure is not None and structure.has_field('buffering-mode'):
if structure['buffering-mode'] == gst.BUFFERING_LIVE: buffering_mode = structure.get_enum(
'buffering-mode', Gst.BufferingMode)
if buffering_mode == Gst.BufferingMode.LIVE:
return # Live sources stall in paused. return # Live sources stall in paused.
level = logging.getLevelName('TRACE') level = logging.getLevelName('TRACE')
if percent < 10 and not self._audio._buffering: if percent < 10 and not self._audio._buffering:
self._audio._playbin.set_state(gst.STATE_PAUSED) self._audio._playbin.set_state(Gst.State.PAUSED)
self._audio._buffering = True self._audio._buffering = True
level = logging.DEBUG level = logging.DEBUG
if percent == 100: if percent == 100:
self._audio._buffering = False self._audio._buffering = False
if self._audio._target_state == gst.STATE_PLAYING: if self._audio._target_state == Gst.State.PLAYING:
self._audio._playbin.set_state(gst.STATE_PLAYING) self._audio._playbin.set_state(Gst.State.PLAYING)
level = logging.DEBUG level = logging.DEBUG
gst_logger.log(level, 'Got buffering message: percent=%d%%', percent) gst_logger.log(
level, 'Got BUFFERING bus message: percent=%d%%', percent)
def on_end_of_stream(self): def on_end_of_stream(self):
gst_logger.debug('Got end-of-stream message.') gst_logger.debug('Got EOS (end of stream) bus message.')
logger.debug('Audio event: reached_end_of_stream()') logger.debug('Audio event: reached_end_of_stream()')
self._audio._tags = {} self._audio._tags = {}
AudioListener.send('reached_end_of_stream') AudioListener.send('reached_end_of_stream')
def on_error(self, error, debug): def on_error(self, error, debug):
gst_logger.error(str(error).decode('utf-8')) error_msg = str(error).decode('utf-8')
if debug: debug_msg = debug.decode('utf-8')
gst_logger.debug(debug.decode('utf-8')) gst_logger.debug(
'Got ERROR bus message: error=%r debug=%r', error_msg, debug_msg)
gst_logger.error('GStreamer error: %s', error_msg)
# TODO: is this needed? # TODO: is this needed?
self._audio.stop_playback() self._audio.stop_playback()
def on_warning(self, error, debug): def on_warning(self, error, debug):
gst_logger.warning(str(error).decode('utf-8')) error_msg = str(error).decode('utf-8')
if debug: debug_msg = debug.decode('utf-8')
gst_logger.debug(debug.decode('utf-8')) gst_logger.warning('GStreamer warning: %s', error_msg)
gst_logger.debug(
'Got WARNING bus message: error=%r debug=%r', error_msg, debug_msg)
def on_async_done(self): def on_async_done(self):
gst_logger.debug('Got async-done.') gst_logger.debug('Got ASYNC_DONE bus message.')
def on_tag(self, taglist): def on_tag(self, taglist):
tags = utils.convert_taglist(taglist) tags = tags_lib.convert_taglist(taglist)
gst_logger.debug('Got TAG bus message: tags=%r', dict(tags))
self._audio._tags.update(tags) self._audio._tags.update(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())
def on_missing_plugin(self, msg): def on_missing_plugin(self, msg):
desc = gst.pbutils.missing_plugin_message_get_description(msg) desc = GstPbutils.missing_plugin_message_get_description(msg)
debug = gst.pbutils.missing_plugin_message_get_installer_detail(msg) debug = GstPbutils.missing_plugin_message_get_installer_detail(msg)
gst_logger.debug(
gst_logger.debug('Got missing-plugin message: description:%s', desc) 'Got missing-plugin bus message: description=%r', desc)
logger.warning('Could not find a %s to handle media.', desc) logger.warning('Could not find a %s to handle media.', desc)
if gst.pbutils.install_plugins_supported(): if GstPbutils.install_plugins_supported():
logger.info('You might be able to fix this by running: ' logger.info('You might be able to fix this by running: '
'gst-installer "%s"', debug) 'gst-installer "%s"', debug)
# TODO: store the missing plugins installer info in a file so we can # TODO: store the missing plugins installer info in a file so we can
# can provide a 'mopidy install-missing-plugins' if the system has the # can provide a 'mopidy install-missing-plugins' if the system has the
# required helper installed? # required helper installed?
def on_new_segment(self, update, rate, format_, start, stop, position): def on_stream_start(self):
gst_logger.debug('Got new-segment event: update=%s rate=%s format=%s ' gst_logger.debug('Got STREAM_START bus message')
'start=%s stop=%s position=%s', update, rate, uri = self._audio._pending_uri
format_.value_name, start, stop, position) logger.debug('Audio event: stream_changed(uri=%r)', uri)
position_ms = position // gst.MSECOND
logger.debug('Audio event: position_changed(position=%s)', position_ms)
AudioListener.send('position_changed', position=position_ms)
def on_stream_changed(self, uri):
gst_logger.debug('Got stream-changed message: uri=%s', uri)
logger.debug('Audio event: stream_changed(uri=%s)', uri)
AudioListener.send('stream_changed', uri=uri) AudioListener.send('stream_changed', uri=uri)
def on_segment(self, segment):
gst_logger.debug(
'Got SEGMENT pad event: '
'rate=%(rate)s format=%(format)s start=%(start)s stop=%(stop)s '
'position=%(position)s', {
'rate': segment.rate,
'format': Gst.Format.get_name(segment.format),
'start': segment.start,
'stop': segment.stop,
'position': segment.position
})
position_ms = segment.position // Gst.MSECOND
logger.debug('Audio event: position_changed(position=%r)', position_ms)
AudioListener.send('position_changed', position=position_ms)
# TODO: create a player class which replaces the actors internals # TODO: create a player class which replaces the actors internals
class Audio(pykka.ThreadingActor): class Audio(pykka.ThreadingActor):
@ -391,9 +378,10 @@ class Audio(pykka.ThreadingActor):
super(Audio, self).__init__() super(Audio, self).__init__()
self._config = config self._config = config
self._target_state = gst.STATE_NULL self._target_state = Gst.State.NULL
self._buffering = False self._buffering = False
self._tags = {} self._tags = {}
self._pending_uri = None
self._playbin = None self._playbin = None
self._outputs = None self._outputs = None
@ -401,7 +389,7 @@ class Audio(pykka.ThreadingActor):
self._handler = _Handler(self) self._handler = _Handler(self)
self._appsrc = _Appsrc() self._appsrc = _Appsrc()
self._signals = _Signals() self._signals = utils.Signals()
if mixer and self._config['audio']['mixer'] == 'software': if mixer and self._config['audio']['mixer'] == 'software':
self.mixer = SoftwareMixer(mixer) self.mixer = SoftwareMixer(mixer)
@ -413,7 +401,7 @@ class Audio(pykka.ThreadingActor):
self._setup_playbin() self._setup_playbin()
self._setup_outputs() self._setup_outputs()
self._setup_audio_sink() self._setup_audio_sink()
except gobject.GError as ex: except GObject.GError as ex:
logger.exception(ex) logger.exception(ex)
process.exit_process() process.exit_process()
@ -424,19 +412,18 @@ class Audio(pykka.ThreadingActor):
def _setup_preferences(self): def _setup_preferences(self):
# TODO: move out of audio actor? # TODO: move out of audio actor?
# Fix for https://github.com/mopidy/mopidy/issues/604 # Fix for https://github.com/mopidy/mopidy/issues/604
registry = gst.registry_get_default() registry = Gst.Registry.get()
jacksink = registry.find_feature( jacksink = registry.find_feature('jackaudiosink', Gst.ElementFactory)
'jackaudiosink', gst.TYPE_ELEMENT_FACTORY)
if jacksink: if jacksink:
jacksink.set_rank(gst.RANK_SECONDARY) jacksink.set_rank(Gst.Rank.SECONDARY)
def _setup_playbin(self): def _setup_playbin(self):
playbin = gst.element_factory_make('playbin2') playbin = Gst.ElementFactory.make('playbin')
playbin.set_property('flags', 2) # GST_PLAY_FLAG_AUDIO playbin.set_property('flags', 2) # GST_PLAY_FLAG_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
playbin.set_property('buffer-duration', 5 * gst.SECOND) playbin.set_property('buffer-duration', 5 * Gst.SECOND)
self._signals.connect(playbin, 'source-setup', self._on_source_setup) self._signals.connect(playbin, 'source-setup', self._on_source_setup)
self._signals.connect(playbin, 'about-to-finish', self._signals.connect(playbin, 'about-to-finish',
@ -450,13 +437,13 @@ class Audio(pykka.ThreadingActor):
self._handler.teardown_event_handling() self._handler.teardown_event_handling()
self._signals.disconnect(self._playbin, 'about-to-finish') self._signals.disconnect(self._playbin, 'about-to-finish')
self._signals.disconnect(self._playbin, 'source-setup') self._signals.disconnect(self._playbin, 'source-setup')
self._playbin.set_state(gst.STATE_NULL) self._playbin.set_state(Gst.State.NULL)
def _setup_outputs(self): def _setup_outputs(self):
# We don't want to use outputs for regular testing, so just install # We don't want to use outputs for regular testing, so just install
# an unsynced fakesink when someone asks for a 'testoutput'. # an unsynced fakesink when someone asks for a 'testoutput'.
if self._config['audio']['output'] == 'testoutput': if self._config['audio']['output'] == 'testoutput':
self._outputs = gst.element_factory_make('fakesink') self._outputs = Gst.ElementFactory.make('fakesink')
else: else:
self._outputs = _Outputs() self._outputs = _Outputs()
try: try:
@ -464,26 +451,25 @@ class Audio(pykka.ThreadingActor):
except exceptions.AudioException: except exceptions.AudioException:
process.exit_process() # TODO: move this up the chain process.exit_process() # TODO: move this up the chain
self._handler.setup_event_handling(self._outputs.get_pad('sink')) self._handler.setup_event_handling(
self._outputs.get_static_pad('sink'))
def _setup_audio_sink(self): def _setup_audio_sink(self):
audio_sink = gst.Bin('audio-sink') audio_sink = Gst.ElementFactory.make('bin', 'audio-sink')
# 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: make the min-max values a setting? # TODO: See if settings should be set to minimize latency. Previous
queue = gst.element_factory_make('queue') # setting breaks appsrc, and settings before that broke on a few
queue.set_property('max-size-buffers', 0) # systems. So leave the default to play it safe.
queue.set_property('max-size-bytes', 0) queue = Gst.ElementFactory.make('queue')
queue.set_property('max-size-time', 3 * gst.SECOND)
queue.set_property('min-threshold-time', 1 * gst.SECOND)
audio_sink.add(queue) audio_sink.add(queue)
audio_sink.add(self._outputs) audio_sink.add(self._outputs)
if self.mixer: if self.mixer:
volume = gst.element_factory_make('volume') volume = Gst.ElementFactory.make('volume')
audio_sink.add(volume) audio_sink.add(volume)
queue.link(volume) queue.link(volume)
volume.link(self._outputs) volume.link(self._outputs)
@ -491,7 +477,7 @@ class Audio(pykka.ThreadingActor):
else: else:
queue.link(self._outputs) queue.link(self._outputs)
ghost_pad = gst.GhostPad('sink', queue.get_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)
self._playbin.set_property('audio-sink', audio_sink) self._playbin.set_property('audio-sink', audio_sink)
@ -508,11 +494,12 @@ class Audio(pykka.ThreadingActor):
gst_logger.debug('Got about-to-finish event.') gst_logger.debug('Got about-to-finish event.')
if self._about_to_finish_callback: if self._about_to_finish_callback:
logger.debug('Running about to finish callback.') logger.debug('Running about-to-finish callback.')
self._about_to_finish_callback() self._about_to_finish_callback()
def _on_source_setup(self, element, source): def _on_source_setup(self, element, source):
gst_logger.debug('Got source-setup: element=%s', source) gst_logger.debug(
'Got source-setup signal: element=%s', source.__class__.__name__)
if source.get_factory().get_name() == 'appsrc': if source.get_factory().get_name() == 'appsrc':
self._appsrc.configure(source) self._appsrc.configure(source)
@ -539,6 +526,7 @@ class Audio(pykka.ThreadingActor):
current_volume = None current_volume = None
self._tags = {} # TODO: add test for this somehow self._tags = {} # TODO: add test for this somehow
self._pending_uri = uri
self._playbin.set_property('uri', uri) self._playbin.set_property('uri', uri)
if self.mixer is not None and current_volume is not None: if self.mixer is not None and current_volume is not None:
@ -563,8 +551,10 @@ class Audio(pykka.ThreadingActor):
:type seek_data: callable which takes time position in ms :type seek_data: callable which takes time position in ms
""" """
self._appsrc.prepare( self._appsrc.prepare(
gst.Caps(bytes(caps)), need_data, enough_data, seek_data) Gst.Caps.from_string(caps), need_data, enough_data, seek_data)
self._playbin.set_property('uri', 'appsrc://') uri = 'appsrc://'
self._pending_uri = uri
self._playbin.set_property('uri', uri)
def emit_data(self, buffer_): def emit_data(self, buffer_):
""" """
@ -579,7 +569,7 @@ class Audio(pykka.ThreadingActor):
Returns :class:`True` if data was delivered. Returns :class:`True` if data was delivered.
:param buffer_: buffer to pass to appsrc :param buffer_: buffer to pass to appsrc
:type buffer_: :class:`gst.Buffer` or :class:`None` :type buffer_: :class:`Gst.Buffer` or :class:`None`
:rtype: boolean :rtype: boolean
""" """
return self._appsrc.push(buffer_) return self._appsrc.push(buffer_)
@ -617,15 +607,16 @@ class Audio(pykka.ThreadingActor):
:rtype: int :rtype: int
""" """
try: success, position = self._playbin.query_position(Gst.Format.TIME)
gst_position = self._playbin.query_position(gst.FORMAT_TIME)[0]
return utils.clocktime_to_millisecond(gst_position) if not success:
except gst.QueryError:
# TODO: take state into account for this and possibly also return # TODO: take state into account for this and possibly also return
# None as the unknown value instead of zero? # None as the unknown value instead of zero?
logger.debug('Position query failed') logger.debug('Position query failed')
return 0 return 0
return utils.clocktime_to_millisecond(position)
def set_position(self, position): def set_position(self, position):
""" """
Set position in milliseconds. Set position in milliseconds.
@ -636,9 +627,9 @@ class Audio(pykka.ThreadingActor):
""" """
# TODO: double check seek flags in use. # TODO: double check seek flags in use.
gst_position = utils.millisecond_to_clocktime(position) gst_position = utils.millisecond_to_clocktime(position)
gst_logger.debug('Sending flushing seek: position=%r', gst_position)
result = self._playbin.seek_simple( result = self._playbin.seek_simple(
gst.Format(gst.FORMAT_TIME), gst.SEEK_FLAG_FLUSH, gst_position) Gst.Format.TIME, Gst.SeekFlags.FLUSH, gst_position)
gst_logger.debug('Sent flushing seek: position=%s', gst_position)
return result return result
def start_playback(self): def start_playback(self):
@ -647,7 +638,7 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False` :rtype: :class:`True` if successfull, else :class:`False`
""" """
return self._set_state(gst.STATE_PLAYING) return self._set_state(Gst.State.PLAYING)
def pause_playback(self): def pause_playback(self):
""" """
@ -655,7 +646,7 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False` :rtype: :class:`True` if successfull, else :class:`False`
""" """
return self._set_state(gst.STATE_PAUSED) return self._set_state(Gst.State.PAUSED)
def prepare_change(self): def prepare_change(self):
""" """
@ -664,9 +655,9 @@ class Audio(pykka.ThreadingActor):
This function *MUST* be called before changing URIs or doing This function *MUST* be called before changing URIs or doing
changes like updating data that is being pushed. The reason for this changes like updating data that is being pushed. The reason for this
is that GStreamer will reset all its state when it changes to is that GStreamer will reset all its state when it changes to
:attr:`gst.STATE_READY`. :attr:`Gst.State.READY`.
""" """
return self._set_state(gst.STATE_READY) return self._set_state(Gst.State.READY)
def stop_playback(self): def stop_playback(self):
""" """
@ -675,14 +666,14 @@ class Audio(pykka.ThreadingActor):
:rtype: :class:`True` if successfull, else :class:`False` :rtype: :class:`True` if successfull, else :class:`False`
""" """
self._buffering = False self._buffering = False
return self._set_state(gst.STATE_NULL) return self._set_state(Gst.State.NULL)
def wait_for_state_change(self): def wait_for_state_change(self):
"""Block until any pending state changes are complete. """Block until any pending state changes are complete.
Should only be used by tests. Should only be used by tests.
""" """
self._playbin.get_state() self._playbin.get_state(timeout=Gst.CLOCK_TIME_NONE)
def enable_sync_handler(self): def enable_sync_handler(self):
"""Enable manual processing of messages from bus. """Enable manual processing of messages from bus.
@ -691,7 +682,7 @@ class Audio(pykka.ThreadingActor):
""" """
def sync_handler(bus, message): def sync_handler(bus, message):
self._handler.on_message(bus, message) self._handler.on_message(bus, message)
return gst.BUS_DROP return Gst.BusSyncReply.DROP
bus = self._playbin.get_bus() bus = self._playbin.get_bus()
bus.set_sync_handler(sync_handler) bus.set_sync_handler(sync_handler)
@ -712,17 +703,18 @@ class Audio(pykka.ThreadingActor):
"READY" -> "NULL" "READY" -> "NULL"
"READY" -> "PAUSED" "READY" -> "PAUSED"
:param state: State to set playbin to. One of: `gst.STATE_NULL`, :param state: State to set playbin to. One of: `Gst.State.NULL`,
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`. `Gst.State.READY`, `Gst.State.PAUSED` and `Gst.State.PLAYING`.
:type state: :class:`gst.State` :type state: :class:`Gst.State`
:rtype: :class:`True` if successfull, else :class:`False` :rtype: :class:`True` if successfull, else :class:`False`
""" """
self._target_state = state self._target_state = state
result = self._playbin.set_state(state) result = self._playbin.set_state(state)
gst_logger.debug('State change to %s: result=%s', state.value_name, gst_logger.debug(
'Changing state to %s: result=%s', state.value_name,
result.value_name) result.value_name)
if result == gst.STATE_CHANGE_FAILURE: if result == Gst.StateChangeReturn.FAILURE:
logger.warning( logger.warning(
'Setting GStreamer state to %s failed', state.value_name) 'Setting GStreamer state to %s failed', state.value_name)
return False return False
@ -735,35 +727,45 @@ class Audio(pykka.ThreadingActor):
""" """
Set track metadata for currently playing song. Set track metadata for currently playing song.
Only needs to be called by sources such as `appsrc` which do not Only needs to be called by sources such as ``appsrc`` which do not
already inject tags in playbin, e.g. when using :meth:`emit_data` to already inject tags in playbin, e.g. when using :meth:`emit_data` to
deliver raw audio data to GStreamer. deliver raw audio data to GStreamer.
:param track: the current track :param track: the current track
:type track: :class:`mopidy.models.Track` :type track: :class:`mopidy.models.Track`
""" """
taglist = gst.TagList() taglist = Gst.TagList.new_empty()
artists = [a for a in (track.artists or []) if a.name] artists = [a for a in (track.artists or []) if a.name]
def set_value(tag, value):
gobject_value = GObject.Value()
gobject_value.init(GObject.TYPE_STRING)
gobject_value.set_string(value)
taglist.add_value(
Gst.TagMergeMode.REPLACE, Gst.TAG_ARTIST, gobject_value)
# Default to blank data to trick shoutcast into clearing any previous # Default to blank data to trick shoutcast into clearing any previous
# values it might have. # values it might have.
taglist[gst.TAG_ARTIST] = ' ' # TODO: Verify if this works at all, likely it doesn't.
taglist[gst.TAG_TITLE] = ' ' set_value(Gst.TAG_ARTIST, ' ')
taglist[gst.TAG_ALBUM] = ' ' set_value(Gst.TAG_TITLE, ' ')
set_value(Gst.TAG_ALBUM, ' ')
if artists: if artists:
taglist[gst.TAG_ARTIST] = ', '.join([a.name for a in artists]) set_value(Gst.TAG_ARTIST, ', '.join([a.name for a in artists]))
if track.name: if track.name:
taglist[gst.TAG_TITLE] = track.name set_value(Gst.TAG_TITLE, track.name)
if track.album and track.album.name: if track.album and track.album.name:
taglist[gst.TAG_ALBUM] = track.album.name set_value(Gst.TAG_ALBUM, track.album.name)
event = gst.event_new_tag(taglist) gst_logger.debug(
'Sending TAG event for track %r: %r',
track.uri, taglist.to_string())
event = Gst.Event.new_tag(taglist)
# TODO: check if we get this back on our own bus? # TODO: check if we get this back on our own bus?
self._playbin.send_event(event) self._playbin.send_event(event)
gst_logger.debug('Sent tag event: track=%s', track.uri)
def get_current_tags(self): def get_current_tags(self):
""" """

View File

@ -1,63 +0,0 @@
from __future__ import absolute_import, unicode_literals
import gobject
import pygst
pygst.require('0.10')
import gst # noqa
class IcySrc(gst.Bin, gst.URIHandler):
__gstdetails__ = ('IcySrc',
'Src',
'HTTP src wrapper for icy:// support.',
'Mopidy')
srcpad_template = gst.PadTemplate(
'src', gst.PAD_SRC, gst.PAD_ALWAYS,
gst.caps_new_any())
__gsttemplates__ = (srcpad_template,)
def __init__(self):
super(IcySrc, self).__init__()
self._httpsrc = gst.element_make_from_uri(gst.URI_SRC, 'http://')
try:
self._httpsrc.set_property('iradio-mode', True)
except TypeError:
pass
self.add(self._httpsrc)
self._srcpad = gst.GhostPad('src', self._httpsrc.get_pad('src'))
self.add_pad(self._srcpad)
@classmethod
def do_get_type_full(cls):
return gst.URI_SRC
@classmethod
def do_get_protocols_full(cls):
return [b'icy', b'icyx']
def do_set_uri(self, uri):
if uri.startswith('icy://'):
return self._httpsrc.set_uri(b'http://' + uri[len('icy://'):])
elif uri.startswith('icyx://'):
return self._httpsrc.set_uri(b'https://' + uri[len('icyx://'):])
else:
return False
def do_get_uri(self):
uri = self._httpsrc.get_uri()
if uri.startswith('http://'):
return b'icy://' + uri[len('http://'):]
else:
return b'icyx://' + uri[len('https://'):]
def register():
# Only register icy if gst install can't handle it on it's own.
if not gst.element_make_from_uri(gst.URI_SRC, 'icy://'):
gobject.type_register(IcySrc)
gst.element_register(
IcySrc, IcySrc.__name__.lower(), gst.RANK_MARGINAL)

View File

@ -2,21 +2,27 @@ from __future__ import (
absolute_import, division, print_function, unicode_literals) absolute_import, division, print_function, unicode_literals)
import collections import collections
import time
import pygst
pygst.require('0.10')
import gst # noqa
import gst.pbutils # noqa
from mopidy import exceptions from mopidy import exceptions
from mopidy.audio import utils from mopidy.audio import tags as tags_lib, utils
from mopidy.internal import encoding from mopidy.internal import encoding
from mopidy.internal.gi import Gst, GstPbutils
# GST_ELEMENT_FACTORY_LIST:
_DECODER = 1 << 0
_AUDIO = 1 << 50
_DEMUXER = 1 << 5
_DEPAYLOADER = 1 << 8
_PARSER = 1 << 6
# GST_TYPE_AUTOPLUG_SELECT_RESULT:
_SELECT_TRY = 0
_SELECT_EXPOSE = 1
_Result = collections.namedtuple( _Result = collections.namedtuple(
'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable')) 'Result', ('uri', 'tags', 'duration', 'seekable', 'mime', 'playable'))
_RAW_AUDIO = gst.Caps(b'audio/x-raw-int; audio/x-raw-float')
# TODO: replace with a scan(uri, timeout=1000, proxy_config=None)? # TODO: replace with a scan(uri, timeout=1000, proxy_config=None)?
class Scanner(object): class Scanner(object):
@ -51,7 +57,7 @@ class Scanner(object):
""" """
timeout = int(timeout or self._timeout_ms) timeout = int(timeout or self._timeout_ms)
tags, duration, seekable, mime = None, None, None, None tags, duration, seekable, mime = None, None, None, None
pipeline = _setup_pipeline(uri, self._proxy_config) pipeline, signals = _setup_pipeline(uri, self._proxy_config)
try: try:
_start_pipeline(pipeline) _start_pipeline(pipeline)
@ -59,7 +65,8 @@ class Scanner(object):
duration = _query_duration(pipeline) duration = _query_duration(pipeline)
seekable = _query_seekable(pipeline) seekable = _query_seekable(pipeline)
finally: finally:
pipeline.set_state(gst.STATE_NULL) signals.clear()
pipeline.set_state(Gst.State.NULL)
del pipeline del pipeline
return _Result(uri, tags, duration, seekable, mime, have_audio) return _Result(uri, tags, duration, seekable, mime, have_audio)
@ -68,117 +75,149 @@ class Scanner(object):
# Turns out it's _much_ faster to just create a new pipeline for every as # Turns out it's _much_ faster to just create a new pipeline for every as
# decodebins and other elements don't seem to take well to being reused. # decodebins and other elements don't seem to take well to being reused.
def _setup_pipeline(uri, proxy_config=None): def _setup_pipeline(uri, proxy_config=None):
src = gst.element_make_from_uri(gst.URI_SRC, uri) src = Gst.Element.make_from_uri(Gst.URIType.SRC, uri)
if not src: if not src:
raise exceptions.ScannerError('GStreamer can not open: %s' % uri) raise exceptions.ScannerError('GStreamer can not open: %s' % uri)
typefind = gst.element_factory_make('typefind') typefind = Gst.ElementFactory.make('typefind')
decodebin = gst.element_factory_make('decodebin2') decodebin = Gst.ElementFactory.make('decodebin')
pipeline = gst.element_factory_make('pipeline') pipeline = Gst.ElementFactory.make('pipeline')
for e in (src, typefind, decodebin): for e in (src, typefind, decodebin):
pipeline.add(e) pipeline.add(e)
gst.element_link_many(src, typefind, decodebin) src.link(typefind)
typefind.link(decodebin)
if proxy_config: if proxy_config:
utils.setup_proxy(src, proxy_config) utils.setup_proxy(src, proxy_config)
typefind.connect('have-type', _have_type, decodebin) signals = utils.Signals()
decodebin.connect('pad-added', _pad_added, pipeline) signals.connect(typefind, 'have-type', _have_type, decodebin)
signals.connect(decodebin, 'pad-added', _pad_added, pipeline)
signals.connect(decodebin, 'autoplug-select', _autoplug_select)
return pipeline return pipeline, signals
def _have_type(element, probability, caps, decodebin): def _have_type(element, probability, caps, decodebin):
decodebin.set_property('sink-caps', caps) decodebin.set_property('sink-caps', caps)
struct = gst.Structure('have-type') struct = Gst.Structure.new_empty('have-type')
struct['caps'] = caps.get_structure(0) struct.set_value('caps', caps.get_structure(0))
element.get_bus().post(gst.message_new_application(element, struct)) element.get_bus().post(Gst.Message.new_application(element, struct))
def _pad_added(element, pad, pipeline): def _pad_added(element, pad, pipeline):
sink = gst.element_factory_make('fakesink') sink = Gst.ElementFactory.make('fakesink')
sink.set_property('sync', False) sink.set_property('sync', False)
pipeline.add(sink) pipeline.add(sink)
sink.sync_state_with_parent() sink.sync_state_with_parent()
pad.link(sink.get_pad('sink')) pad.link(sink.get_static_pad('sink'))
if pad.get_caps().is_subset(_RAW_AUDIO): if pad.query_caps().is_subset(Gst.Caps.from_string('audio/x-raw')):
struct = gst.Structure('have-audio') # Probably won't happen due to autoplug-select fix, but lets play it
element.get_bus().post(gst.message_new_application(element, struct)) # safe until we've tested more.
struct = Gst.Structure.new_empty('have-audio')
element.get_bus().post(Gst.Message.new_application(element, struct))
def _autoplug_select(element, pad, caps, factory):
if factory.list_is_type(_DECODER | _AUDIO):
struct = Gst.Structure.new_empty('have-audio')
element.get_bus().post(Gst.Message.new_application(element, struct))
if not factory.list_is_type(_DEMUXER | _DEPAYLOADER | _PARSER):
return _SELECT_EXPOSE
return _SELECT_TRY
def _start_pipeline(pipeline): def _start_pipeline(pipeline):
if pipeline.set_state(gst.STATE_PAUSED) == gst.STATE_CHANGE_NO_PREROLL: result = pipeline.set_state(Gst.State.PAUSED)
pipeline.set_state(gst.STATE_PLAYING) if result == Gst.StateChangeReturn.NO_PREROLL:
pipeline.set_state(Gst.State.PLAYING)
def _query_duration(pipeline): def _query_duration(pipeline, timeout=100):
try: # 1. Try and get a duration, return if success.
duration = pipeline.query_duration(gst.FORMAT_TIME, None)[0] # 2. Some formats need to play some buffers before duration is found.
except gst.QueryError: # 3. Wait for a duration change event.
# 4. Try and get a duration again.
success, duration = pipeline.query_duration(Gst.Format.TIME)
if success and duration >= 0:
return duration // Gst.MSECOND
result = pipeline.set_state(Gst.State.PLAYING)
if result == Gst.StateChangeReturn.FAILURE:
return None return None
if duration < 0: 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 return None
else:
return duration // gst.MSECOND
def _query_seekable(pipeline): def _query_seekable(pipeline):
query = gst.query_new_seeking(gst.FORMAT_TIME) query = Gst.Query.new_seeking(Gst.Format.TIME)
pipeline.query(query) pipeline.query(query)
return query.parse_seeking()[1] return query.parse_seeking()[1]
def _process(pipeline, timeout_ms): def _process(pipeline, timeout_ms):
clock = pipeline.get_clock()
bus = pipeline.get_bus() bus = pipeline.get_bus()
timeout = timeout_ms * gst.MSECOND
tags = {} tags = {}
mime = None mime = None
have_audio = False have_audio = False
missing_message = None missing_message = None
types = (gst.MESSAGE_ELEMENT | gst.MESSAGE_APPLICATION | types = (
gst.MESSAGE_ERROR | gst.MESSAGE_EOS | gst.MESSAGE_ASYNC_DONE | Gst.MessageType.ELEMENT |
gst.MESSAGE_TAG) Gst.MessageType.APPLICATION |
Gst.MessageType.ERROR |
Gst.MessageType.EOS |
Gst.MessageType.ASYNC_DONE |
Gst.MessageType.TAG
)
previous = clock.get_time() timeout = timeout_ms
previous = int(time.time() * 1000)
while timeout > 0: while timeout > 0:
message = bus.timed_pop_filtered(timeout, types) message = bus.timed_pop_filtered(timeout * Gst.MSECOND, types)
if message is None: if message is None:
break break
elif message.type == gst.MESSAGE_ELEMENT: elif message.type == Gst.MessageType.ELEMENT:
if gst.pbutils.is_missing_plugin_message(message): if GstPbutils.is_missing_plugin_message(message):
missing_message = message missing_message = message
elif message.type == gst.MESSAGE_APPLICATION: elif message.type == Gst.MessageType.APPLICATION:
if message.structure.get_name() == 'have-type': if message.get_structure().get_name() == 'have-type':
mime = message.structure['caps'].get_name() mime = message.get_structure().get_value('caps').get_name()
if mime.startswith('text/') or mime == 'application/xml': if mime and (
mime.startswith('text/') or mime == 'application/xml'):
return tags, mime, have_audio return tags, mime, have_audio
elif message.structure.get_name() == 'have-audio': elif message.get_structure().get_name() == 'have-audio':
have_audio = True have_audio = True
elif message.type == gst.MESSAGE_ERROR: elif message.type == Gst.MessageType.ERROR:
error = encoding.locale_decode(message.parse_error()[0]) error = encoding.locale_decode(message.parse_error()[0])
if missing_message and not mime: if missing_message and not mime:
caps = missing_message.structure['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
raise exceptions.ScannerError(error) raise exceptions.ScannerError(error)
elif message.type == gst.MESSAGE_EOS: elif message.type == Gst.MessageType.EOS:
return tags, mime, have_audio return tags, mime, have_audio
elif message.type == gst.MESSAGE_ASYNC_DONE: elif message.type == Gst.MessageType.ASYNC_DONE:
if message.src == pipeline: if message.src == pipeline:
return tags, mime, have_audio return tags, mime, have_audio
elif message.type == gst.MESSAGE_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(utils.convert_taglist(taglist)) tags.update(tags_lib.convert_taglist(taglist))
now = clock.get_time() now = int(time.time() * 1000)
timeout -= now - previous timeout -= now - previous
previous = now previous = now
@ -189,15 +228,11 @@ if __name__ == '__main__':
import os import os
import sys import sys
import gobject
from mopidy.internal import path from mopidy.internal import path
gobject.threads_init()
scanner = Scanner(5000) scanner = Scanner(5000)
for uri in sys.argv[1:]: for uri in sys.argv[1:]:
if not gst.uri_is_valid(uri): if not Gst.uri_is_valid(uri):
uri = path.path_to_uri(os.path.abspath(uri)) uri = path.path_to_uri(os.path.abspath(uri))
try: try:
result = scanner.scan(uri) result = scanner.scan(uri)

139
mopidy/audio/tags.py Normal file
View File

@ -0,0 +1,139 @@
from __future__ import absolute_import, unicode_literals
import collections
import datetime
import logging
import numbers
from mopidy import compat
from mopidy.internal import log
from mopidy.internal.gi import GLib, Gst
from mopidy.models import Album, Artist, Track
logger = logging.getLogger(__name__)
def convert_taglist(taglist):
"""Convert a :class:`Gst.TagList` to plain Python types.
Knows how to convert:
- Dates
- Buffers
- Numbers
- Strings
- Booleans
Unknown types will be ignored and trace logged. Tag keys are all strings
defined as part GStreamer under GstTagList_.
.. _GstTagList: https://developer.gnome.org/gstreamer/stable/\
gstreamer-GstTagList.html
:param taglist: A GStreamer taglist to be converted.
:type taglist: :class:`Gst.TagList`
:rtype: dictionary of tag keys with a list of values.
"""
result = collections.defaultdict(list)
for n in range(taglist.n_tags()):
tag = taglist.nth_tag_name(n)
for i in range(taglist.get_tag_size(tag)):
value = taglist.get_value_index(tag, i)
if isinstance(value, GLib.Date):
date = datetime.date(
value.get_year(), value.get_month(), value.get_day())
result[tag].append(date.isoformat().decode('utf-8'))
if isinstance(value, Gst.DateTime):
result[tag].append(value.to_iso8601_string().decode('utf-8'))
elif isinstance(value, bytes):
result[tag].append(value.decode('utf-8', 'replace'))
elif isinstance(value, (compat.text_type, bool, numbers.Number)):
result[tag].append(value)
else:
logger.log(
log.TRACE_LOG_LEVEL,
'Ignoring unknown tag data: %r = %r', tag, value)
return result
# TODO: split based on "stream" and "track" based conversion? i.e. handle data
# from radios in it's own helper instead?
def convert_tags_to_track(tags):
"""Convert our normalized tags to a track.
:param tags: dictionary of tag keys with a list of values
:type tags: :class:`dict`
:rtype: :class:`mopidy.models.Track`
"""
album_kwargs = {}
track_kwargs = {}
track_kwargs['composers'] = _artists(tags, Gst.TAG_COMPOSER)
track_kwargs['performers'] = _artists(tags, Gst.TAG_PERFORMER)
track_kwargs['artists'] = _artists(tags, Gst.TAG_ARTIST,
'musicbrainz-artistid',
'musicbrainz-sortname')
album_kwargs['artists'] = _artists(
tags, Gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid')
track_kwargs['genre'] = '; '.join(tags.get(Gst.TAG_GENRE, []))
track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_TITLE, []))
if not track_kwargs['name']:
track_kwargs['name'] = '; '.join(tags.get(Gst.TAG_ORGANIZATION, []))
track_kwargs['comment'] = '; '.join(tags.get('comment', []))
if not track_kwargs['comment']:
track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_LOCATION, []))
if not track_kwargs['comment']:
track_kwargs['comment'] = '; '.join(tags.get(Gst.TAG_COPYRIGHT, []))
track_kwargs['track_no'] = tags.get(Gst.TAG_TRACK_NUMBER, [None])[0]
track_kwargs['disc_no'] = tags.get(Gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0]
track_kwargs['bitrate'] = tags.get(Gst.TAG_BITRATE, [None])[0]
track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0]
album_kwargs['name'] = tags.get(Gst.TAG_ALBUM, [None])[0]
album_kwargs['num_tracks'] = tags.get(Gst.TAG_TRACK_COUNT, [None])[0]
album_kwargs['num_discs'] = tags.get(Gst.TAG_ALBUM_VOLUME_COUNT, [None])[0]
album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0]
album_kwargs['date'] = tags.get(Gst.TAG_DATE, [None])[0]
if not album_kwargs['date']:
datetime = tags.get(Gst.TAG_DATE_TIME, [None])[0]
if datetime is not None:
album_kwargs['date'] = datetime.split('T')[0]
# Clear out any empty values we found
track_kwargs = {k: v for k, v in track_kwargs.items() if v}
album_kwargs = {k: v for k, v in album_kwargs.items() if v}
# Only bother with album if we have a name to show.
if album_kwargs.get('name'):
track_kwargs['album'] = Album(**album_kwargs)
return Track(**track_kwargs)
def _artists(
tags, artist_name, artist_id=None, artist_sortname=None):
# Name missing, don't set artist
if not tags.get(artist_name):
return None
# One artist name and either id or sortname, include all available fields
if len(tags[artist_name]) == 1 and \
(artist_id in tags or artist_sortname in tags):
attrs = {'name': tags[artist_name][0]}
if artist_id in tags:
attrs['musicbrainz_id'] = tags[artist_id][0]
if artist_sortname in tags:
attrs['sortname'] = tags[artist_sortname][0]
return [Artist(**attrs)]
# Multiple artist, provide artists with name only to avoid ambiguity.
return [Artist(name=name) for name in tags[artist_name]]

View File

@ -1,50 +1,41 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import datetime from mopidy import httpclient
import logging from mopidy.internal.gi import Gst
import numbers
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy import compat, httpclient
from mopidy.models import Album, Artist, Track
logger = logging.getLogger(__name__)
def calculate_duration(num_samples, sample_rate): def calculate_duration(num_samples, sample_rate):
"""Determine duration of samples using GStreamer helper for precise """Determine duration of samples using GStreamer helper for precise
math.""" math."""
return gst.util_uint64_scale(num_samples, gst.SECOND, sample_rate) return Gst.util_uint64_scale(num_samples, Gst.SECOND, sample_rate)
def create_buffer(data, capabilites=None, timestamp=None, duration=None): def create_buffer(data, capabilites=None, timestamp=None, duration=None):
"""Create a new GStreamer buffer based on provided data. """Create a new GStreamer buffer based on provided data.
Mainly intended to keep gst imports out of non-audio modules. Mainly intended to keep gst imports out of non-audio modules.
.. versionchanged:: 1.2
``capabilites`` argument is no longer in use
""" """
buffer_ = gst.Buffer(data) if not data:
if capabilites: raise ValueError('Cannot create buffer without data')
if isinstance(capabilites, compat.string_types): buffer_ = Gst.Buffer.new_wrapped(data)
capabilites = gst.caps_from_string(capabilites) if timestamp is not None:
buffer_.set_caps(capabilites) buffer_.pts = timestamp
if timestamp: if duration is not None:
buffer_.timestamp = timestamp
if duration:
buffer_.duration = duration buffer_.duration = duration
return buffer_ return buffer_
def millisecond_to_clocktime(value): def millisecond_to_clocktime(value):
"""Convert a millisecond time to internal GStreamer time.""" """Convert a millisecond time to internal GStreamer time."""
return value * gst.MSECOND return value * Gst.MSECOND
def clocktime_to_millisecond(value): def clocktime_to_millisecond(value):
"""Convert an internal GStreamer time to millisecond time.""" """Convert an internal GStreamer time to millisecond time."""
return value // gst.MSECOND return value // Gst.MSECOND
def supported_uri_schemes(uri_schemes): def supported_uri_schemes(uri_schemes):
@ -55,9 +46,9 @@ def supported_uri_schemes(uri_schemes):
:rtype: set of URI schemes we can support via this GStreamer install. :rtype: set of URI schemes we can support via this GStreamer install.
""" """
supported_schemes = set() supported_schemes = set()
registry = gst.registry_get_default() registry = Gst.Registry.get()
for factory in registry.get_feature_list(gst.TYPE_ELEMENT_FACTORY): for factory in registry.get_feature_list(Gst.ElementFactory):
for uri in factory.get_uri_protocols(): for uri in factory.get_uri_protocols():
if uri in uri_schemes: if uri in uri_schemes:
supported_schemes.add(uri) supported_schemes.add(uri)
@ -65,84 +56,11 @@ def supported_uri_schemes(uri_schemes):
return supported_schemes return supported_schemes
def _artists(tags, artist_name, artist_id=None, artist_sortname=None):
# Name missing, don't set artist
if not tags.get(artist_name):
return None
# One artist name and either id or sortname, include all available fields
if len(tags[artist_name]) == 1 and \
(artist_id in tags or artist_sortname in tags):
attrs = {'name': tags[artist_name][0]}
if artist_id in tags:
attrs['musicbrainz_id'] = tags[artist_id][0]
if artist_sortname in tags:
attrs['sortname'] = tags[artist_sortname][0]
return [Artist(**attrs)]
# Multiple artist, provide artists with name only to avoid ambiguity.
return [Artist(name=name) for name in tags[artist_name]]
# TODO: split based on "stream" and "track" based conversion? i.e. handle data
# from radios in it's own helper instead?
def convert_tags_to_track(tags):
"""Convert our normalized tags to a track.
:param tags: dictionary of tag keys with a list of values
:type tags: :class:`dict`
:rtype: :class:`mopidy.models.Track`
"""
album_kwargs = {}
track_kwargs = {}
track_kwargs['composers'] = _artists(tags, gst.TAG_COMPOSER)
track_kwargs['performers'] = _artists(tags, gst.TAG_PERFORMER)
track_kwargs['artists'] = _artists(tags, gst.TAG_ARTIST,
'musicbrainz-artistid',
'musicbrainz-sortname')
album_kwargs['artists'] = _artists(
tags, gst.TAG_ALBUM_ARTIST, 'musicbrainz-albumartistid')
track_kwargs['genre'] = '; '.join(tags.get(gst.TAG_GENRE, []))
track_kwargs['name'] = '; '.join(tags.get(gst.TAG_TITLE, []))
if not track_kwargs['name']:
track_kwargs['name'] = '; '.join(tags.get(gst.TAG_ORGANIZATION, []))
track_kwargs['comment'] = '; '.join(tags.get('comment', []))
if not track_kwargs['comment']:
track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_LOCATION, []))
if not track_kwargs['comment']:
track_kwargs['comment'] = '; '.join(tags.get(gst.TAG_COPYRIGHT, []))
track_kwargs['track_no'] = tags.get(gst.TAG_TRACK_NUMBER, [None])[0]
track_kwargs['disc_no'] = tags.get(gst.TAG_ALBUM_VOLUME_NUMBER, [None])[0]
track_kwargs['bitrate'] = tags.get(gst.TAG_BITRATE, [None])[0]
track_kwargs['musicbrainz_id'] = tags.get('musicbrainz-trackid', [None])[0]
album_kwargs['name'] = tags.get(gst.TAG_ALBUM, [None])[0]
album_kwargs['num_tracks'] = tags.get(gst.TAG_TRACK_COUNT, [None])[0]
album_kwargs['num_discs'] = tags.get(gst.TAG_ALBUM_VOLUME_COUNT, [None])[0]
album_kwargs['musicbrainz_id'] = tags.get('musicbrainz-albumid', [None])[0]
if tags.get(gst.TAG_DATE) and tags.get(gst.TAG_DATE)[0]:
track_kwargs['date'] = tags[gst.TAG_DATE][0].isoformat()
# Clear out any empty values we found
track_kwargs = {k: v for k, v in track_kwargs.items() if v}
album_kwargs = {k: v for k, v in album_kwargs.items() if v}
# Only bother with album if we have a name to show.
if album_kwargs.get('name'):
track_kwargs['album'] = Album(**album_kwargs)
return Track(**track_kwargs)
def setup_proxy(element, config): def setup_proxy(element, config):
"""Configure a GStreamer element with proxy settings. """Configure a GStreamer element with proxy settings.
:param element: element to setup proxy in. :param element: element to setup proxy in.
:type element: :class:`gst.GstElement` :type element: :class:`Gst.GstElement`
:param config: proxy settings to use. :param config: proxy settings to use.
:type config: :class:`dict` :type config: :class:`dict`
""" """
@ -154,51 +72,31 @@ def setup_proxy(element, config):
element.set_property('proxy-pw', config.get('password')) element.set_property('proxy-pw', config.get('password'))
def convert_taglist(taglist): class Signals(object):
"""Convert a :class:`gst.Taglist` to plain Python types.
Knows how to convert: """Helper for tracking gobject signal registrations"""
- Dates def __init__(self):
- Buffers self._ids = {}
- Numbers
- Strings
- Booleans
Unknown types will be ignored and debug logged. Tag keys are all strings def connect(self, element, event, func, *args):
defined as part GStreamer under GstTagList_. """Connect a function + args to signal event on an element.
.. _GstTagList: http://gstreamer.freedesktop.org/data/doc/gstreamer/\ Each event may only be handled by one callback in this implementation.
0.10.36/gstreamer/html/gstreamer-GstTagList.html
:param taglist: A GStreamer taglist to be converted.
:type taglist: :class:`gst.Taglist`
:rtype: dictionary of tag keys with a list of values.
""" """
result = {} assert (element, event) not in self._ids
self._ids[(element, event)] = element.connect(event, func, *args)
# Taglists are not really dicts, hence the lack of .items() and def disconnect(self, element, event):
# explicit use of .keys() """Disconnect whatever handler we have for an element+event pair.
for key in taglist.keys():
result.setdefault(key, [])
values = taglist[key] Does nothing it the handler has already been removed.
if not isinstance(values, list): """
values = [values] signal_id = self._ids.pop((element, event), None)
if signal_id is not None:
element.disconnect(signal_id)
for value in values: def clear(self):
if isinstance(value, gst.Date): """Clear all registered signal handlers."""
try: for element, event in self._ids.keys():
date = datetime.date(value.year, value.month, value.day) element.disconnect(self._ids.pop((element, event)))
result[key].append(date)
except ValueError:
logger.debug('Ignoring invalid date: %r = %r', key, value)
elif isinstance(value, gst.Buffer):
result[key].append(bytes(value))
elif isinstance(
value, (compat.string_types, bool, numbers.Number)):
result[key].append(value)
else:
logger.debug('Ignoring unknown data: %r = %r', key, value)
return result

View File

@ -7,9 +7,7 @@ import logging
import os import os
import sys import sys
import glib from gi.repository import GLib, GObject
import gobject
import pykka import pykka
@ -21,7 +19,7 @@ from mopidy.internal import deps, process, timer, versioning
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_default_config = [] _default_config = []
for base in glib.get_system_config_dirs() + (glib.get_user_config_dir(),): for base in GLib.get_system_config_dirs() + [GLib.get_user_config_dir()]:
_default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf')) _default_config.append(os.path.join(base, b'mopidy', b'mopidy.conf'))
DEFAULT_CONFIG = b':'.join(_default_config) DEFAULT_CONFIG = b':'.join(_default_config)
@ -286,7 +284,7 @@ class RootCommand(Command):
help='`section/key=value` values to override config options') help='`section/key=value` values to override config options')
def run(self, args, config): def run(self, args, config):
loop = gobject.MainLoop() loop = GObject.MainLoop()
mixer_class = self.get_mixer_class(config, args.registry['mixer']) mixer_class = self.get_mixer_class(config, args.registry['mixer'])
backend_classes = args.registry['backend'] backend_classes = args.registry['backend']

View File

@ -236,7 +236,9 @@ class LibraryController(object):
result = future.get() result = future.get()
if result is not None: if result is not None:
validation.check_instances(result, models.Track) 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: if uri:
return results[uri] return results[uri]

View File

@ -486,7 +486,7 @@ class PlaybackController(object):
if time_position < 0: if time_position < 0:
time_position = 0 time_position = 0
elif time_position > tl_track.track.length: elif time_position > tl_track.track.length:
# TODO: gstreamer will trigger a about to finish for us, use that? # TODO: GStreamer will trigger a about-to-finish for us, use that?
self.next() self.next()
return True return True

View File

@ -7,7 +7,7 @@ import sys
import urllib2 import urllib2
from mopidy import backend, exceptions, models from mopidy import backend, exceptions, models
from mopidy.audio import scan, utils from mopidy.audio import scan, tags
from mopidy.internal import path from mopidy.internal import path
@ -83,7 +83,7 @@ class FileLibraryProvider(backend.LibraryProvider):
try: try:
result = self._scanner.scan(uri) result = self._scanner.scan(uri)
track = utils.convert_tags_to_track(result.tags).copy( track = tags.convert_tags_to_track(result.tags).copy(
uri=uri, length=result.duration) uri=uri, length=result.duration)
except exceptions.ScannerError as e: except exceptions.ScannerError as e:
logger.warning('Failed looking up %s: %s', uri, e) logger.warning('Failed looking up %s: %s', uri, e)

View File

@ -7,11 +7,8 @@ import sys
import pkg_resources import pkg_resources
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy.internal import formatting from mopidy.internal import formatting
from mopidy.internal.gi import Gst, gi
def format_dependency_list(adapters=None): def format_dependency_list(adapters=None):
@ -110,8 +107,7 @@ def pkg_info(project_name=None, include_extras=False):
def gstreamer_info(): def gstreamer_info():
other = [] other = []
other.append('Python wrapper: gst-python %s' % ( other.append('Python wrapper: python-gi %s' % gi.__version__)
'.'.join(map(str, gst.get_pygst_version()))))
found_elements = [] found_elements = []
missing_elements = [] missing_elements = []
@ -135,8 +131,8 @@ def gstreamer_info():
return { return {
'name': 'GStreamer', 'name': 'GStreamer',
'version': '.'.join(map(str, gst.get_gst_version())), 'version': '.'.join(map(str, Gst.version())),
'path': os.path.dirname(gst.__file__), 'path': os.path.dirname(gi.__file__),
'other': '\n'.join(other), 'other': '\n'.join(other),
} }
@ -187,6 +183,6 @@ def _gstreamer_check_elements():
] ]
known_elements = [ known_elements = [
factory.get_name() for factory in factory.get_name() for factory in
gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)] Gst.Registry.get().get_feature_list(Gst.ElementFactory)]
return [ return [
(element, element in known_elements) for element in elements_to_check] (element, element in known_elements) for element in elements_to_check]

42
mopidy/internal/gi.py Normal file
View File

@ -0,0 +1,42 @@
from __future__ import absolute_import, print_function, unicode_literals
import sys
import textwrap
try:
import gi
gi.require_version('Gst', '1.0')
gi.require_version('GstPbutils', '1.0')
from gi.repository import GLib, GObject, Gst, GstPbutils
except ImportError:
print(textwrap.dedent("""
ERROR: A GObject Python package was not found.
Mopidy requires GStreamer to work. GStreamer is a C library with a
number of dependencies itself, and cannot be installed with the regular
Python tools like pip.
Please see http://docs.mopidy.com/en/latest/installation/ for
instructions on how to install the required dependencies.
"""))
raise
else:
Gst.is_initialized() or Gst.init()
REQUIRED_GST_VERSION = (1, 2)
if Gst.version() < REQUIRED_GST_VERSION:
sys.exit(
'ERROR: Mopidy requires GStreamer >= %s, but found %s.' % (
'.'.join(map(str, REQUIRED_GST_VERSION)), Gst.version_string()))
__all__ = [
'GLib',
'GObject',
'Gst',
'GstPbutils',
'gi',
]

View File

@ -7,7 +7,7 @@ import socket
import sys import sys
import threading import threading
import gobject from gi.repository import GObject
import pykka import pykka
@ -67,7 +67,7 @@ def format_hostname(hostname):
class Server(object): class Server(object):
"""Setup listener and register it with gobject's event loop.""" """Setup listener and register it with GObject's event loop."""
def __init__(self, host, port, protocol, protocol_kwargs=None, def __init__(self, host, port, protocol, protocol_kwargs=None,
max_connections=5, timeout=30): max_connections=5, timeout=30):
@ -87,7 +87,7 @@ class Server(object):
return sock return sock
def register_server_socket(self, fileno): def register_server_socket(self, fileno):
gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection) GObject.io_add_watch(fileno, GObject.IO_IN, self.handle_connection)
def handle_connection(self, fd, flags): def handle_connection(self, fd, flags):
try: try:
@ -132,7 +132,7 @@ class Server(object):
class Connection(object): class Connection(object):
# NOTE: the callback code is _not_ run in the actor's thread, but in the # NOTE: the callback code is _not_ run in the actor's thread, but in the
# same one as the event loop. If code in the callbacks blocks, the rest of # same one as the event loop. If code in the callbacks blocks, the rest of
# gobject code will likely be blocked as well... # GObject code will likely be blocked as well...
# #
# Also note that source_remove() return values are ignored on purpose, a # Also note that source_remove() return values are ignored on purpose, a
# false return value would only tell us that what we thought was registered # false return value would only tell us that what we thought was registered
@ -211,14 +211,14 @@ class Connection(object):
return return
self.disable_timeout() self.disable_timeout()
self.timeout_id = gobject.timeout_add_seconds( self.timeout_id = GObject.timeout_add_seconds(
self.timeout, self.timeout_callback) self.timeout, self.timeout_callback)
def disable_timeout(self): def disable_timeout(self):
"""Deactivate timeout mechanism.""" """Deactivate timeout mechanism."""
if self.timeout_id is None: if self.timeout_id is None:
return return
gobject.source_remove(self.timeout_id) GObject.source_remove(self.timeout_id)
self.timeout_id = None self.timeout_id = None
def enable_recv(self): def enable_recv(self):
@ -226,9 +226,9 @@ class Connection(object):
return return
try: try:
self.recv_id = gobject.io_add_watch( self.recv_id = GObject.io_add_watch(
self.sock.fileno(), self.sock.fileno(),
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP,
self.recv_callback) self.recv_callback)
except socket.error as e: except socket.error as e:
self.stop('Problem with connection: %s' % e) self.stop('Problem with connection: %s' % e)
@ -236,7 +236,7 @@ class Connection(object):
def disable_recv(self): def disable_recv(self):
if self.recv_id is None: if self.recv_id is None:
return return
gobject.source_remove(self.recv_id) GObject.source_remove(self.recv_id)
self.recv_id = None self.recv_id = None
def enable_send(self): def enable_send(self):
@ -244,9 +244,9 @@ class Connection(object):
return return
try: try:
self.send_id = gobject.io_add_watch( self.send_id = GObject.io_add_watch(
self.sock.fileno(), self.sock.fileno(),
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP,
self.send_callback) self.send_callback)
except socket.error as e: except socket.error as e:
self.stop('Problem with connection: %s' % e) self.stop('Problem with connection: %s' % e)
@ -255,11 +255,11 @@ class Connection(object):
if self.send_id is None: if self.send_id is None:
return return
gobject.source_remove(self.send_id) GObject.source_remove(self.send_id)
self.send_id = None self.send_id = None
def recv_callback(self, fd, flags): def recv_callback(self, fd, flags):
if flags & (gobject.IO_ERR | gobject.IO_HUP): if flags & (GObject.IO_ERR | GObject.IO_HUP):
self.stop('Bad client flags: %s' % flags) self.stop('Bad client flags: %s' % flags)
return True return True
@ -283,7 +283,7 @@ class Connection(object):
return True return True
def send_callback(self, fd, flags): def send_callback(self, fd, flags):
if flags & (gobject.IO_ERR | gobject.IO_HUP): if flags & (GObject.IO_ERR | GObject.IO_HUP):
self.stop('Bad client flags: %s' % flags) self.stop('Bad client flags: %s' % flags)
return True return True

View File

@ -2,10 +2,6 @@ from __future__ import absolute_import, unicode_literals
import io import io
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy.compat import configparser from mopidy.compat import configparser
from mopidy.internal import validation from mopidy.internal import validation

View File

@ -4,13 +4,14 @@ import contextlib
import logging import logging
import time import time
from mopidy.internal import log
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TRACE = logging.getLevelName('TRACE')
@contextlib.contextmanager @contextlib.contextmanager
def time_logger(name, level=TRACE): def time_logger(name, level=log.TRACE_LOG_LEVEL):
start = time.time() start = time.time()
yield yield
logger.log(level, '%s took %dms', name, (time.time() - start) * 1000) logger.log(level, '%s took %dms', name, (time.time() - start) * 1000)

View File

@ -6,7 +6,7 @@ import os
import time import time
from mopidy import commands, compat, exceptions from mopidy import commands, compat, exceptions
from mopidy.audio import scan, utils from mopidy.audio import scan, tags
from mopidy.internal import path from mopidy.internal import path
from mopidy.local import translator from mopidy.local import translator
@ -140,18 +140,18 @@ class ScanCommand(commands.Command):
relpath = translator.local_track_uri_to_path(uri, media_dir) relpath = translator.local_track_uri_to_path(uri, media_dir)
file_uri = path.path_to_uri(os.path.join(media_dir, relpath)) file_uri = path.path_to_uri(os.path.join(media_dir, relpath))
result = scanner.scan(file_uri) result = scanner.scan(file_uri)
tags, duration = result.tags, result.duration
if not result.playable: if not result.playable:
logger.warning('Failed %s: No audio found in file.', uri) logger.warning('Failed %s: No audio found in file.', uri)
elif duration < MIN_DURATION_MS: elif result.duration < MIN_DURATION_MS:
logger.warning('Failed %s: Track shorter than %dms', logger.warning('Failed %s: Track shorter than %dms',
uri, MIN_DURATION_MS) uri, MIN_DURATION_MS)
else: else:
mtime = file_mtimes.get(os.path.join(media_dir, relpath)) mtime = file_mtimes.get(os.path.join(media_dir, relpath))
track = utils.convert_tags_to_track(tags).replace( track = tags.convert_tags_to_track(result.tags).replace(
uri=uri, length=duration, last_modified=mtime) uri=uri, length=result.duration, last_modified=mtime)
if library.add_supports_tags_and_duration: if library.add_supports_tags_and_duration:
library.add(track, tags=tags, duration=duration) library.add(
track, tags=result.tags, duration=result.duration)
else: else:
library.add(track) library.add(track)
logger.debug('Added %s', track.uri) logger.debug('Added %s', track.uri)

View File

@ -42,11 +42,11 @@ def path_to_local_track_uri(relpath):
URI.""" URI."""
if isinstance(relpath, compat.text_type): if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8') relpath = relpath.encode('utf-8')
return b'local:track:%s' % urllib.quote(relpath) return 'local:track:%s' % urllib.quote(relpath)
def path_to_local_directory_uri(relpath): def path_to_local_directory_uri(relpath):
"""Convert path relative to :confval:`local/media_dir` directory URI.""" """Convert path relative to :confval:`local/media_dir` directory URI."""
if isinstance(relpath, compat.text_type): if isinstance(relpath, compat.text_type):
relpath = relpath.encode('utf-8') relpath = relpath.encode('utf-8')
return b'local:directory:%s' % urllib.quote(relpath) return 'local:directory:%s' % urllib.quote(relpath)

View File

@ -353,14 +353,14 @@ class SearchResult(ValidatedImmutableObject):
:type albums: list of :class:`Album` elements :type albums: list of :class:`Album` elements
""" """
# The search result URI. Read-only. #: The search result URI. Read-only.
uri = fields.URI() 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) 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) 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) albums = fields.Collection(type=Album, container=tuple)

View File

@ -426,3 +426,27 @@ def stop(context):
Stops playing. Stops playing.
""" """
context.core.playback.stop() 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 from __future__ import absolute_import, unicode_literals
import datetime import datetime
import logging
import re import re
from mopidy.models import TlTrack from mopidy.models import TlTrack
from mopidy.mpd.protocol import tagtype_list from mopidy.mpd.protocol import tagtype_list
logger = logging.getLogger(__name__)
# TODO: special handling of local:// uri scheme # TODO: special handling of local:// uri scheme
normalize_path_re = re.compile(r'[^/]+') normalize_path_re = re.compile(r'[^/]+')
@ -34,8 +38,12 @@ def track_to_mpd_format(track, position=None, stream_title=None):
else: else:
(tlid, track) = (None, track) (tlid, track) = (None, track)
if not track.uri:
logger.warning('Ignoring track without uri')
return []
result = [ result = [
('file', track.uri or ''), ('file', track.uri),
('Time', track.length and (track.length // 1000) or 0), ('Time', track.length and (track.length // 1000) or 0),
('Artist', concat_multi_values(track.artists, 'name')), ('Artist', concat_multi_values(track.artists, 'name')),
('Album', track.album and track.album.name or ''), ('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) assert len(tracks) == len(positions)
result = [] result = []
for track, position in zip(tracks, positions): 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 return result

View File

@ -8,7 +8,7 @@ import time
import pykka import pykka
from mopidy import audio as audio_lib, backend, exceptions, stream from mopidy import audio as audio_lib, backend, exceptions, stream
from mopidy.audio import scan, utils from mopidy.audio import scan, tags
from mopidy.compat import urllib from mopidy.compat import urllib
from mopidy.internal import http, playlists from mopidy.internal import http, playlists
from mopidy.models import Track from mopidy.models import Track
@ -60,7 +60,7 @@ class StreamLibraryProvider(backend.LibraryProvider):
try: try:
result = self._scanner.scan(uri) result = self._scanner.scan(uri)
track = utils.convert_tags_to_track(result.tags).replace( track = tags.convert_tags_to_track(result.tags).replace(
uri=uri, length=result.duration) uri=uri, length=result.duration)
except exceptions.ScannerError as e: except exceptions.ScannerError as e:
logger.warning('Problem looking up %s: %s', uri, e) logger.warning('Problem looking up %s: %s', uri, e)

View File

@ -3,20 +3,14 @@ from __future__ import absolute_import, unicode_literals
import threading import threading
import unittest import unittest
import gobject
gobject.threads_init()
import mock import mock
import pygst
pygst.require('0.10')
import gst # noqa
import pykka import pykka
from mopidy import audio from mopidy import audio
from mopidy.audio.constants import PlaybackState from mopidy.audio.constants import PlaybackState
from mopidy.internal import path from mopidy.internal import path
from mopidy.internal.gi import Gst
from tests import dummy_audio, path_to_data_dir from tests import dummy_audio, path_to_data_dir
@ -520,17 +514,17 @@ class AudioStateTest(unittest.TestCase):
def test_state_does_not_change_when_in_gst_ready_state(self): def test_state_does_not_change_when_in_gst_ready_state(self):
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING) Gst.State.NULL, Gst.State.READY, Gst.State.VOID_PENDING)
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
def test_state_changes_from_stopped_to_playing_on_play(self): def test_state_changes_from_stopped_to_playing_on_play(self):
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING) Gst.State.NULL, Gst.State.READY, Gst.State.PLAYING)
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING) Gst.State.READY, Gst.State.PAUSED, Gst.State.PLAYING)
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING) Gst.State.PAUSED, Gst.State.PLAYING, Gst.State.VOID_PENDING)
self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state)
@ -538,7 +532,7 @@ class AudioStateTest(unittest.TestCase):
self.audio.state = audio.PlaybackState.PLAYING self.audio.state = audio.PlaybackState.PLAYING
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING) Gst.State.PLAYING, Gst.State.PAUSED, Gst.State.VOID_PENDING)
self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state)
@ -546,12 +540,12 @@ class AudioStateTest(unittest.TestCase):
self.audio.state = audio.PlaybackState.PLAYING self.audio.state = audio.PlaybackState.PLAYING
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL) Gst.State.PLAYING, Gst.State.PAUSED, Gst.State.NULL)
self.audio._handler.on_playbin_state_changed( self.audio._handler.on_playbin_state_changed(
gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL) Gst.State.PAUSED, Gst.State.READY, Gst.State.NULL)
# We never get the following call, so the logic must work without it # We never get the following call, so the logic must work without it
# self.audio._handler.on_playbin_state_changed( # self.audio._handler.on_playbin_state_changed(
# gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING) # Gst.State.READY, Gst.State.NULL, Gst.State.VOID_PENDING)
self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state)
@ -565,17 +559,17 @@ class AudioBufferingTest(unittest.TestCase):
def test_pause_when_buffer_empty(self): def test_pause_when_buffer_empty(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.start_playback() self.audio.start_playback()
playbin.set_state.assert_called_with(gst.STATE_PLAYING) playbin.set_state.assert_called_with(Gst.State.PLAYING)
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(0) self.audio._handler.on_buffering(0)
playbin.set_state.assert_called_with(gst.STATE_PAUSED) playbin.set_state.assert_called_with(Gst.State.PAUSED)
self.assertTrue(self.audio._buffering) self.assertTrue(self.audio._buffering)
def test_stay_paused_when_buffering_finished(self): def test_stay_paused_when_buffering_finished(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.pause_playback() self.audio.pause_playback()
playbin.set_state.assert_called_with(gst.STATE_PAUSED) playbin.set_state.assert_called_with(Gst.State.PAUSED)
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(100) self.audio._handler.on_buffering(100)
@ -585,11 +579,11 @@ class AudioBufferingTest(unittest.TestCase):
def test_change_to_paused_while_buffering(self): def test_change_to_paused_while_buffering(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.start_playback() self.audio.start_playback()
playbin.set_state.assert_called_with(gst.STATE_PLAYING) playbin.set_state.assert_called_with(Gst.State.PLAYING)
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(0) self.audio._handler.on_buffering(0)
playbin.set_state.assert_called_with(gst.STATE_PAUSED) playbin.set_state.assert_called_with(Gst.State.PAUSED)
self.audio.pause_playback() self.audio.pause_playback()
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
@ -600,13 +594,13 @@ class AudioBufferingTest(unittest.TestCase):
def test_change_to_stopped_while_buffering(self): def test_change_to_stopped_while_buffering(self):
playbin = self.audio._playbin playbin = self.audio._playbin
self.audio.start_playback() self.audio.start_playback()
playbin.set_state.assert_called_with(gst.STATE_PLAYING) playbin.set_state.assert_called_with(Gst.State.PLAYING)
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
self.audio._handler.on_buffering(0) self.audio._handler.on_buffering(0)
playbin.set_state.assert_called_with(gst.STATE_PAUSED) playbin.set_state.assert_called_with(Gst.State.PAUSED)
playbin.set_state.reset_mock() playbin.set_state.reset_mock()
self.audio.stop_playback() self.audio.stop_playback()
playbin.set_state.assert_called_with(gst.STATE_NULL) playbin.set_state.assert_called_with(Gst.State.NULL)
self.assertFalse(self.audio._buffering) self.assertFalse(self.audio._buffering)

View File

@ -3,9 +3,6 @@ from __future__ import absolute_import, unicode_literals
import os import os
import unittest import unittest
import gobject
gobject.threads_init()
from mopidy import exceptions from mopidy import exceptions
from mopidy.audio import scan from mopidy.audio import scan
from mopidy.internal import path as path_lib from mopidy.internal import path as path_lib

333
tests/audio/test_tags.py Normal file
View File

@ -0,0 +1,333 @@
# encoding: utf-8
from __future__ import absolute_import, unicode_literals
import unittest
from mopidy import compat
from mopidy.audio import tags
from mopidy.internal.gi import GLib, GObject, Gst
from mopidy.models import Album, Artist, Track
class TestConvertTaglist(object):
def make_taglist(self, tag, values):
taglist = Gst.TagList.new_empty()
for value in values:
if isinstance(value, (GLib.Date, Gst.DateTime)):
taglist.add_value(Gst.TagMergeMode.APPEND, tag, value)
continue
gobject_value = GObject.Value()
if isinstance(value, bytes):
gobject_value.init(GObject.TYPE_STRING)
gobject_value.set_string(value)
elif isinstance(value, int):
gobject_value.init(GObject.TYPE_UINT)
gobject_value.set_uint(value)
gobject_value.init(GObject.TYPE_VALUE)
gobject_value.set_value(value)
else:
raise TypeError
taglist.add_value(Gst.TagMergeMode.APPEND, tag, gobject_value)
return taglist
def test_date_tag(self):
date = GLib.Date.new_dmy(7, 1, 2014)
taglist = self.make_taglist(Gst.TAG_DATE, [date])
result = tags.convert_taglist(taglist)
assert isinstance(result[Gst.TAG_DATE][0], compat.text_type)
assert result[Gst.TAG_DATE][0] == '2014-01-07'
def test_date_time_tag(self):
taglist = self.make_taglist(Gst.TAG_DATE_TIME, [
Gst.DateTime.new_from_iso8601_string(b'2014-01-07 14:13:12')
])
result = tags.convert_taglist(taglist)
assert isinstance(result[Gst.TAG_DATE_TIME][0], compat.text_type)
assert result[Gst.TAG_DATE_TIME][0] == '2014-01-07T14:13:12Z'
def test_string_tag(self):
taglist = self.make_taglist(Gst.TAG_ARTIST, [b'ABBA', b'ACDC'])
result = tags.convert_taglist(taglist)
assert isinstance(result[Gst.TAG_ARTIST][0], compat.text_type)
assert result[Gst.TAG_ARTIST][0] == 'ABBA'
assert isinstance(result[Gst.TAG_ARTIST][1], compat.text_type)
assert result[Gst.TAG_ARTIST][1] == 'ACDC'
def test_integer_tag(self):
taglist = self.make_taglist(Gst.TAG_BITRATE, [17])
result = tags.convert_taglist(taglist)
assert result[Gst.TAG_BITRATE][0] == 17
# TODO: keep ids without name?
# TODO: current test is trying to test everything at once with a complete tags
# set, instead we might want to try with a minimal one making testing easier.
class TagsToTrackTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.tags = {
'album': ['album'],
'track-number': [1],
'artist': ['artist'],
'composer': ['composer'],
'performer': ['performer'],
'album-artist': ['albumartist'],
'title': ['track'],
'track-count': [2],
'album-disc-number': [2],
'album-disc-count': [3],
'date': ['2006-01-01'],
'container-format': ['ID3 tag'],
'genre': ['genre'],
'comment': ['comment'],
'musicbrainz-trackid': ['trackid'],
'musicbrainz-albumid': ['albumid'],
'musicbrainz-artistid': ['artistid'],
'musicbrainz-sortname': ['sortname'],
'musicbrainz-albumartistid': ['albumartistid'],
'bitrate': [1000],
}
artist = Artist(name='artist', musicbrainz_id='artistid',
sortname='sortname')
composer = Artist(name='composer')
performer = Artist(name='performer')
albumartist = Artist(name='albumartist',
musicbrainz_id='albumartistid')
album = Album(name='album', date='2006-01-01',
num_tracks=2, num_discs=3,
musicbrainz_id='albumid', artists=[albumartist])
self.track = Track(name='track',
genre='genre', track_no=1, disc_no=2,
comment='comment', musicbrainz_id='trackid',
album=album, bitrate=1000, artists=[artist],
composers=[composer], performers=[performer])
def check(self, expected):
actual = tags.convert_tags_to_track(self.tags)
self.assertEqual(expected, actual)
def test_track(self):
self.check(self.track)
def test_missing_track_no(self):
del self.tags['track-number']
self.check(self.track.replace(track_no=None))
def test_multiple_track_no(self):
self.tags['track-number'].append(9)
self.check(self.track)
def test_missing_track_disc_no(self):
del self.tags['album-disc-number']
self.check(self.track.replace(disc_no=None))
def test_multiple_track_disc_no(self):
self.tags['album-disc-number'].append(9)
self.check(self.track)
def test_missing_track_name(self):
del self.tags['title']
self.check(self.track.replace(name=None))
def test_multiple_track_name(self):
self.tags['title'] = ['name1', 'name2']
self.check(self.track.replace(name='name1; name2'))
def test_missing_track_musicbrainz_id(self):
del self.tags['musicbrainz-trackid']
self.check(self.track.replace(musicbrainz_id=None))
def test_multiple_track_musicbrainz_id(self):
self.tags['musicbrainz-trackid'].append('id')
self.check(self.track)
def test_missing_track_bitrate(self):
del self.tags['bitrate']
self.check(self.track.replace(bitrate=None))
def test_multiple_track_bitrate(self):
self.tags['bitrate'].append(1234)
self.check(self.track)
def test_missing_track_genre(self):
del self.tags['genre']
self.check(self.track.replace(genre=None))
def test_multiple_track_genre(self):
self.tags['genre'] = ['genre1', 'genre2']
self.check(self.track.replace(genre='genre1; genre2'))
def test_missing_track_date(self):
del self.tags['date']
self.check(
self.track.replace(album=self.track.album.replace(date=None)))
def test_multiple_track_date(self):
self.tags['date'].append('2030-01-01')
self.check(self.track)
def test_datetime_instead_of_date(self):
del self.tags['date']
self.tags['datetime'] = ['2006-01-01T14:13:12Z']
self.check(self.track)
def test_missing_track_comment(self):
del self.tags['comment']
self.check(self.track.replace(comment=None))
def test_multiple_track_comment(self):
self.tags['comment'] = ['comment1', 'comment2']
self.check(self.track.replace(comment='comment1; comment2'))
def test_missing_track_artist_name(self):
del self.tags['artist']
self.check(self.track.replace(artists=[]))
def test_multiple_track_artist_name(self):
self.tags['artist'] = ['name1', 'name2']
artists = [Artist(name='name1'), Artist(name='name2')]
self.check(self.track.replace(artists=artists))
def test_missing_track_artist_musicbrainz_id(self):
del self.tags['musicbrainz-artistid']
artist = list(self.track.artists)[0].replace(musicbrainz_id=None)
self.check(self.track.replace(artists=[artist]))
def test_multiple_track_artist_musicbrainz_id(self):
self.tags['musicbrainz-artistid'].append('id')
self.check(self.track)
def test_missing_track_composer_name(self):
del self.tags['composer']
self.check(self.track.replace(composers=[]))
def test_multiple_track_composer_name(self):
self.tags['composer'] = ['composer1', 'composer2']
composers = [Artist(name='composer1'), Artist(name='composer2')]
self.check(self.track.replace(composers=composers))
def test_missing_track_performer_name(self):
del self.tags['performer']
self.check(self.track.replace(performers=[]))
def test_multiple_track_performe_name(self):
self.tags['performer'] = ['performer1', 'performer2']
performers = [Artist(name='performer1'), Artist(name='performer2')]
self.check(self.track.replace(performers=performers))
def test_missing_album_name(self):
del self.tags['album']
self.check(self.track.replace(album=None))
def test_multiple_album_name(self):
self.tags['album'].append('album2')
self.check(self.track)
def test_missing_album_musicbrainz_id(self):
del self.tags['musicbrainz-albumid']
album = self.track.album.replace(musicbrainz_id=None,
images=[])
self.check(self.track.replace(album=album))
def test_multiple_album_musicbrainz_id(self):
self.tags['musicbrainz-albumid'].append('id')
self.check(self.track)
def test_missing_album_num_tracks(self):
del self.tags['track-count']
album = self.track.album.replace(num_tracks=None)
self.check(self.track.replace(album=album))
def test_multiple_album_num_tracks(self):
self.tags['track-count'].append(9)
self.check(self.track)
def test_missing_album_num_discs(self):
del self.tags['album-disc-count']
album = self.track.album.replace(num_discs=None)
self.check(self.track.replace(album=album))
def test_multiple_album_num_discs(self):
self.tags['album-disc-count'].append(9)
self.check(self.track)
def test_missing_album_artist_name(self):
del self.tags['album-artist']
album = self.track.album.replace(artists=[])
self.check(self.track.replace(album=album))
def test_multiple_album_artist_name(self):
self.tags['album-artist'] = ['name1', 'name2']
artists = [Artist(name='name1'), Artist(name='name2')]
album = self.track.album.replace(artists=artists)
self.check(self.track.replace(album=album))
def test_missing_album_artist_musicbrainz_id(self):
del self.tags['musicbrainz-albumartistid']
albumartist = list(self.track.album.artists)[0]
albumartist = albumartist.replace(musicbrainz_id=None)
album = self.track.album.replace(artists=[albumartist])
self.check(self.track.replace(album=album))
def test_multiple_album_artist_musicbrainz_id(self):
self.tags['musicbrainz-albumartistid'].append('id')
self.check(self.track)
def test_stream_organization_track_name(self):
del self.tags['title']
self.tags['organization'] = ['organization']
self.check(self.track.replace(name='organization'))
def test_multiple_organization_track_name(self):
del self.tags['title']
self.tags['organization'] = ['organization1', 'organization2']
self.check(self.track.replace(name='organization1; organization2'))
# TODO: combine all comment types?
def test_stream_location_track_comment(self):
del self.tags['comment']
self.tags['location'] = ['location']
self.check(self.track.replace(comment='location'))
def test_multiple_location_track_comment(self):
del self.tags['comment']
self.tags['location'] = ['location1', 'location2']
self.check(self.track.replace(comment='location1; location2'))
def test_stream_copyright_track_comment(self):
del self.tags['comment']
self.tags['copyright'] = ['copyright']
self.check(self.track.replace(comment='copyright'))
def test_multiple_copyright_track_comment(self):
del self.tags['comment']
self.tags['copyright'] = ['copyright1', 'copyright2']
self.check(self.track.replace(comment='copyright1; copyright2'))
def test_sortname(self):
self.tags['musicbrainz-sortname'] = ['another_sortname']
artist = Artist(name='artist', sortname='another_sortname',
musicbrainz_id='artistid')
self.check(self.track.replace(artists=[artist]))
def test_missing_sortname(self):
del self.tags['musicbrainz-sortname']
artist = Artist(name='artist', sortname=None,
musicbrainz_id='artistid')
self.check(self.track.replace(artists=[artist]))

View File

@ -1,261 +1,23 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import datetime import pytest
import unittest
from mopidy.audio import utils from mopidy.audio import utils
from mopidy.models import Album, Artist, Track from mopidy.internal.gi import Gst
# TODO: keep ids without name? class TestCreateBuffer(object):
# TODO: current test is trying to test everything at once with a complete tags
# set, instead we might want to try with a minimal one making testing easier.
class TagsToTrackTest(unittest.TestCase):
def setUp(self): # noqa: N802 def test_creates_buffer(self):
self.tags = { buf = utils.create_buffer(b'123', timestamp=0, duration=1000000)
'album': ['album'],
'track-number': [1],
'artist': ['artist'],
'composer': ['composer'],
'performer': ['performer'],
'album-artist': ['albumartist'],
'title': ['track'],
'track-count': [2],
'album-disc-number': [2],
'album-disc-count': [3],
'date': [datetime.date(2006, 1, 1,)],
'container-format': ['ID3 tag'],
'genre': ['genre'],
'comment': ['comment'],
'musicbrainz-trackid': ['trackid'],
'musicbrainz-albumid': ['albumid'],
'musicbrainz-artistid': ['artistid'],
'musicbrainz-sortname': ['sortname'],
'musicbrainz-albumartistid': ['albumartistid'],
'bitrate': [1000],
}
artist = Artist(name='artist', musicbrainz_id='artistid', assert isinstance(buf, Gst.Buffer)
sortname='sortname') assert buf.pts == 0
composer = Artist(name='composer') assert buf.duration == 1000000
performer = Artist(name='performer') assert buf.get_size() == len(b'123')
albumartist = Artist(name='albumartist',
musicbrainz_id='albumartistid')
album = Album(name='album', num_tracks=2, num_discs=3, def test_fails_if_data_has_zero_length(self):
musicbrainz_id='albumid', artists=[albumartist]) with pytest.raises(ValueError) as excinfo:
utils.create_buffer(b'', timestamp=0, duration=1000000)
self.track = Track(name='track', date='2006-01-01', assert 'Cannot create buffer without data' in str(excinfo.value)
genre='genre', track_no=1, disc_no=2,
comment='comment', musicbrainz_id='trackid',
album=album, bitrate=1000, artists=[artist],
composers=[composer], performers=[performer])
def check(self, expected):
actual = utils.convert_tags_to_track(self.tags)
self.assertEqual(expected, actual)
def test_track(self):
self.check(self.track)
def test_missing_track_no(self):
del self.tags['track-number']
self.check(self.track.replace(track_no=None))
def test_multiple_track_no(self):
self.tags['track-number'].append(9)
self.check(self.track)
def test_missing_track_disc_no(self):
del self.tags['album-disc-number']
self.check(self.track.replace(disc_no=None))
def test_multiple_track_disc_no(self):
self.tags['album-disc-number'].append(9)
self.check(self.track)
def test_missing_track_name(self):
del self.tags['title']
self.check(self.track.replace(name=None))
def test_multiple_track_name(self):
self.tags['title'] = ['name1', 'name2']
self.check(self.track.replace(name='name1; name2'))
def test_missing_track_musicbrainz_id(self):
del self.tags['musicbrainz-trackid']
self.check(self.track.replace(musicbrainz_id=None))
def test_multiple_track_musicbrainz_id(self):
self.tags['musicbrainz-trackid'].append('id')
self.check(self.track)
def test_missing_track_bitrate(self):
del self.tags['bitrate']
self.check(self.track.replace(bitrate=None))
def test_multiple_track_bitrate(self):
self.tags['bitrate'].append(1234)
self.check(self.track)
def test_missing_track_genre(self):
del self.tags['genre']
self.check(self.track.replace(genre=None))
def test_multiple_track_genre(self):
self.tags['genre'] = ['genre1', 'genre2']
self.check(self.track.replace(genre='genre1; genre2'))
def test_missing_track_date(self):
del self.tags['date']
self.check(self.track.replace(date=None))
def test_multiple_track_date(self):
self.tags['date'].append(datetime.date(2030, 1, 1))
self.check(self.track)
def test_missing_track_comment(self):
del self.tags['comment']
self.check(self.track.replace(comment=None))
def test_multiple_track_comment(self):
self.tags['comment'] = ['comment1', 'comment2']
self.check(self.track.replace(comment='comment1; comment2'))
def test_missing_track_artist_name(self):
del self.tags['artist']
self.check(self.track.replace(artists=[]))
def test_multiple_track_artist_name(self):
self.tags['artist'] = ['name1', 'name2']
artists = [Artist(name='name1'), Artist(name='name2')]
self.check(self.track.replace(artists=artists))
def test_missing_track_artist_musicbrainz_id(self):
del self.tags['musicbrainz-artistid']
artist = list(self.track.artists)[0].replace(musicbrainz_id=None)
self.check(self.track.replace(artists=[artist]))
def test_multiple_track_artist_musicbrainz_id(self):
self.tags['musicbrainz-artistid'].append('id')
self.check(self.track)
def test_missing_track_composer_name(self):
del self.tags['composer']
self.check(self.track.replace(composers=[]))
def test_multiple_track_composer_name(self):
self.tags['composer'] = ['composer1', 'composer2']
composers = [Artist(name='composer1'), Artist(name='composer2')]
self.check(self.track.replace(composers=composers))
def test_missing_track_performer_name(self):
del self.tags['performer']
self.check(self.track.replace(performers=[]))
def test_multiple_track_performe_name(self):
self.tags['performer'] = ['performer1', 'performer2']
performers = [Artist(name='performer1'), Artist(name='performer2')]
self.check(self.track.replace(performers=performers))
def test_missing_album_name(self):
del self.tags['album']
self.check(self.track.replace(album=None))
def test_multiple_album_name(self):
self.tags['album'].append('album2')
self.check(self.track)
def test_missing_album_musicbrainz_id(self):
del self.tags['musicbrainz-albumid']
album = self.track.album.replace(musicbrainz_id=None,
images=[])
self.check(self.track.replace(album=album))
def test_multiple_album_musicbrainz_id(self):
self.tags['musicbrainz-albumid'].append('id')
self.check(self.track)
def test_missing_album_num_tracks(self):
del self.tags['track-count']
album = self.track.album.replace(num_tracks=None)
self.check(self.track.replace(album=album))
def test_multiple_album_num_tracks(self):
self.tags['track-count'].append(9)
self.check(self.track)
def test_missing_album_num_discs(self):
del self.tags['album-disc-count']
album = self.track.album.replace(num_discs=None)
self.check(self.track.replace(album=album))
def test_multiple_album_num_discs(self):
self.tags['album-disc-count'].append(9)
self.check(self.track)
def test_missing_album_artist_name(self):
del self.tags['album-artist']
album = self.track.album.replace(artists=[])
self.check(self.track.replace(album=album))
def test_multiple_album_artist_name(self):
self.tags['album-artist'] = ['name1', 'name2']
artists = [Artist(name='name1'), Artist(name='name2')]
album = self.track.album.replace(artists=artists)
self.check(self.track.replace(album=album))
def test_missing_album_artist_musicbrainz_id(self):
del self.tags['musicbrainz-albumartistid']
albumartist = list(self.track.album.artists)[0]
albumartist = albumartist.replace(musicbrainz_id=None)
album = self.track.album.replace(artists=[albumartist])
self.check(self.track.replace(album=album))
def test_multiple_album_artist_musicbrainz_id(self):
self.tags['musicbrainz-albumartistid'].append('id')
self.check(self.track)
def test_stream_organization_track_name(self):
del self.tags['title']
self.tags['organization'] = ['organization']
self.check(self.track.replace(name='organization'))
def test_multiple_organization_track_name(self):
del self.tags['title']
self.tags['organization'] = ['organization1', 'organization2']
self.check(self.track.replace(name='organization1; organization2'))
# TODO: combine all comment types?
def test_stream_location_track_comment(self):
del self.tags['comment']
self.tags['location'] = ['location']
self.check(self.track.replace(comment='location'))
def test_multiple_location_track_comment(self):
del self.tags['comment']
self.tags['location'] = ['location1', 'location2']
self.check(self.track.replace(comment='location1; location2'))
def test_stream_copyright_track_comment(self):
del self.tags['comment']
self.tags['copyright'] = ['copyright']
self.check(self.track.replace(comment='copyright'))
def test_multiple_copyright_track_comment(self):
del self.tags['comment']
self.tags['copyright'] = ['copyright1', 'copyright2']
self.check(self.track.replace(comment='copyright1; copyright2'))
def test_sortname(self):
self.tags['musicbrainz-sortname'] = ['another_sortname']
artist = Artist(name='artist', sortname='another_sortname',
musicbrainz_id='artistid')
self.check(self.track.replace(artists=[artist]))
def test_missing_sortname(self):
del self.tags['musicbrainz-sortname']
artist = Artist(name='artist', sortname=None,
musicbrainz_id='artistid')
self.check(self.track.replace(artists=[artist]))

View File

@ -153,8 +153,8 @@ class CoreLibraryTest(BaseCoreLibraryTest):
self.core.library.lookup('dummy1:a', ['dummy2:a']) self.core.library.lookup('dummy1:a', ['dummy2:a'])
def test_lookup_can_handle_uris(self): def test_lookup_can_handle_uris(self):
track1 = Track(name='abc') track1 = Track(uri='dummy1:a', name='abc')
track2 = Track(name='def') track2 = Track(uri='dummy2:a', name='def')
self.library1.lookup().get.return_value = [track1] self.library1.lookup().get.return_value = [track1]
self.library2.lookup().get.return_value = [track2] self.library2.lookup().get.return_value = [track2]
@ -169,6 +169,15 @@ class CoreLibraryTest(BaseCoreLibraryTest):
self.assertFalse(self.library1.lookup.called) self.assertFalse(self.library1.lookup.called)
self.assertFalse(self.library2.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): def test_refresh_with_uri_selects_dummy1_backend(self):
self.core.library.refresh('dummy1:a') self.core.library.refresh('dummy1:a')

View File

@ -232,7 +232,7 @@ class TestPreviousHandling(BaseTest):
self.assertIn(tl_tracks[1], self.core.tracklist.tl_tracks) self.assertIn(tl_tracks[1], self.core.tracklist.tl_tracks)
class TestPlayUnknownHanlding(BaseTest): class TestPlayUnknownHandling(BaseTest):
tracks = [Track(uri='unknown:a', length=1234), tracks = [Track(uri='unknown:a', length=1234),
Track(uri='dummy:b', length=1234)] Track(uri='dummy:b', length=1234)]
@ -264,7 +264,7 @@ class TestConsumeHandling(BaseTest):
tl_track = self.core.tracklist.get_tl_tracks()[0] tl_track = self.core.tracklist.get_tl_tracks()[0]
self.core.playback.play(tl_track) self.core.playback.play(tl_track)
self.core.tracklist.consume = True self.core.tracklist.set_consume(True)
self.replay_events() self.replay_events()
self.core.playback.next() self.core.playback.next()

View File

@ -5,7 +5,7 @@ import logging
import socket import socket
import unittest import unittest
import gobject from gi.repository import GObject
from mock import Mock, call, patch, sentinel from mock import Mock, call, patch, sentinel
@ -162,27 +162,27 @@ class ConnectionTest(unittest.TestCase):
network.Connection.stop(self.mock, sentinel.reason) network.Connection.stop(self.mock, sentinel.reason)
network.logger.log(any_int, any_unicode) network.logger.log(any_int, any_unicode)
@patch.object(gobject, 'io_add_watch', new=Mock()) @patch.object(GObject, 'io_add_watch', new=Mock())
def test_enable_recv_registers_with_gobject(self): def test_enable_recv_registers_with_gobject(self):
self.mock.recv_id = None self.mock.recv_id = None
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.sock.fileno.return_value = sentinel.fileno self.mock.sock.fileno.return_value = sentinel.fileno
gobject.io_add_watch.return_value = sentinel.tag GObject.io_add_watch.return_value = sentinel.tag
network.Connection.enable_recv(self.mock) network.Connection.enable_recv(self.mock)
gobject.io_add_watch.assert_called_once_with( GObject.io_add_watch.assert_called_once_with(
sentinel.fileno, sentinel.fileno,
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, GObject.IO_IN | GObject.IO_ERR | GObject.IO_HUP,
self.mock.recv_callback) self.mock.recv_callback)
self.assertEqual(sentinel.tag, self.mock.recv_id) self.assertEqual(sentinel.tag, self.mock.recv_id)
@patch.object(gobject, 'io_add_watch', new=Mock()) @patch.object(GObject, 'io_add_watch', new=Mock())
def test_enable_recv_already_registered(self): def test_enable_recv_already_registered(self):
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.recv_id = sentinel.tag self.mock.recv_id = sentinel.tag
network.Connection.enable_recv(self.mock) network.Connection.enable_recv(self.mock)
self.assertEqual(0, gobject.io_add_watch.call_count) self.assertEqual(0, GObject.io_add_watch.call_count)
def test_enable_recv_does_not_change_tag(self): def test_enable_recv_does_not_change_tag(self):
self.mock.recv_id = sentinel.tag self.mock.recv_id = sentinel.tag
@ -191,20 +191,20 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_recv(self.mock) network.Connection.enable_recv(self.mock)
self.assertEqual(sentinel.tag, self.mock.recv_id) self.assertEqual(sentinel.tag, self.mock.recv_id)
@patch.object(gobject, 'source_remove', new=Mock()) @patch.object(GObject, 'source_remove', new=Mock())
def test_disable_recv_deregisters(self): def test_disable_recv_deregisters(self):
self.mock.recv_id = sentinel.tag self.mock.recv_id = sentinel.tag
network.Connection.disable_recv(self.mock) network.Connection.disable_recv(self.mock)
gobject.source_remove.assert_called_once_with(sentinel.tag) GObject.source_remove.assert_called_once_with(sentinel.tag)
self.assertEqual(None, self.mock.recv_id) self.assertEqual(None, self.mock.recv_id)
@patch.object(gobject, 'source_remove', new=Mock()) @patch.object(GObject, 'source_remove', new=Mock())
def test_disable_recv_already_deregistered(self): def test_disable_recv_already_deregistered(self):
self.mock.recv_id = None self.mock.recv_id = None
network.Connection.disable_recv(self.mock) network.Connection.disable_recv(self.mock)
self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(0, GObject.source_remove.call_count)
self.assertEqual(None, self.mock.recv_id) self.assertEqual(None, self.mock.recv_id)
def test_enable_recv_on_closed_socket(self): def test_enable_recv_on_closed_socket(self):
@ -216,27 +216,27 @@ class ConnectionTest(unittest.TestCase):
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
self.assertEqual(None, self.mock.recv_id) self.assertEqual(None, self.mock.recv_id)
@patch.object(gobject, 'io_add_watch', new=Mock()) @patch.object(GObject, 'io_add_watch', new=Mock())
def test_enable_send_registers_with_gobject(self): def test_enable_send_registers_with_gobject(self):
self.mock.send_id = None self.mock.send_id = None
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.sock.fileno.return_value = sentinel.fileno self.mock.sock.fileno.return_value = sentinel.fileno
gobject.io_add_watch.return_value = sentinel.tag GObject.io_add_watch.return_value = sentinel.tag
network.Connection.enable_send(self.mock) network.Connection.enable_send(self.mock)
gobject.io_add_watch.assert_called_once_with( GObject.io_add_watch.assert_called_once_with(
sentinel.fileno, sentinel.fileno,
gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, GObject.IO_OUT | GObject.IO_ERR | GObject.IO_HUP,
self.mock.send_callback) self.mock.send_callback)
self.assertEqual(sentinel.tag, self.mock.send_id) self.assertEqual(sentinel.tag, self.mock.send_id)
@patch.object(gobject, 'io_add_watch', new=Mock()) @patch.object(GObject, 'io_add_watch', new=Mock())
def test_enable_send_already_registered(self): def test_enable_send_already_registered(self):
self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock = Mock(spec=socket.SocketType)
self.mock.send_id = sentinel.tag self.mock.send_id = sentinel.tag
network.Connection.enable_send(self.mock) network.Connection.enable_send(self.mock)
self.assertEqual(0, gobject.io_add_watch.call_count) self.assertEqual(0, GObject.io_add_watch.call_count)
def test_enable_send_does_not_change_tag(self): def test_enable_send_does_not_change_tag(self):
self.mock.send_id = sentinel.tag self.mock.send_id = sentinel.tag
@ -245,20 +245,20 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_send(self.mock) network.Connection.enable_send(self.mock)
self.assertEqual(sentinel.tag, self.mock.send_id) self.assertEqual(sentinel.tag, self.mock.send_id)
@patch.object(gobject, 'source_remove', new=Mock()) @patch.object(GObject, 'source_remove', new=Mock())
def test_disable_send_deregisters(self): def test_disable_send_deregisters(self):
self.mock.send_id = sentinel.tag self.mock.send_id = sentinel.tag
network.Connection.disable_send(self.mock) network.Connection.disable_send(self.mock)
gobject.source_remove.assert_called_once_with(sentinel.tag) GObject.source_remove.assert_called_once_with(sentinel.tag)
self.assertEqual(None, self.mock.send_id) self.assertEqual(None, self.mock.send_id)
@patch.object(gobject, 'source_remove', new=Mock()) @patch.object(GObject, 'source_remove', new=Mock())
def test_disable_send_already_deregistered(self): def test_disable_send_already_deregistered(self):
self.mock.send_id = None self.mock.send_id = None
network.Connection.disable_send(self.mock) network.Connection.disable_send(self.mock)
self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(0, GObject.source_remove.call_count)
self.assertEqual(None, self.mock.send_id) self.assertEqual(None, self.mock.send_id)
def test_enable_send_on_closed_socket(self): def test_enable_send_on_closed_socket(self):
@ -269,36 +269,36 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_send(self.mock) network.Connection.enable_send(self.mock)
self.assertEqual(None, self.mock.send_id) self.assertEqual(None, self.mock.send_id)
@patch.object(gobject, 'timeout_add_seconds', new=Mock()) @patch.object(GObject, 'timeout_add_seconds', new=Mock())
def test_enable_timeout_clears_existing_timeouts(self): def test_enable_timeout_clears_existing_timeouts(self):
self.mock.timeout = 10 self.mock.timeout = 10
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.mock.disable_timeout.assert_called_once_with() self.mock.disable_timeout.assert_called_once_with()
@patch.object(gobject, 'timeout_add_seconds', new=Mock()) @patch.object(GObject, 'timeout_add_seconds', new=Mock())
def test_enable_timeout_add_gobject_timeout(self): def test_enable_timeout_add_gobject_timeout(self):
self.mock.timeout = 10 self.mock.timeout = 10
gobject.timeout_add_seconds.return_value = sentinel.tag GObject.timeout_add_seconds.return_value = sentinel.tag
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
gobject.timeout_add_seconds.assert_called_once_with( GObject.timeout_add_seconds.assert_called_once_with(
10, self.mock.timeout_callback) 10, self.mock.timeout_callback)
self.assertEqual(sentinel.tag, self.mock.timeout_id) self.assertEqual(sentinel.tag, self.mock.timeout_id)
@patch.object(gobject, 'timeout_add_seconds', new=Mock()) @patch.object(GObject, 'timeout_add_seconds', new=Mock())
def test_enable_timeout_does_not_add_timeout(self): def test_enable_timeout_does_not_add_timeout(self):
self.mock.timeout = 0 self.mock.timeout = 0
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.assertEqual(0, gobject.timeout_add_seconds.call_count) self.assertEqual(0, GObject.timeout_add_seconds.call_count)
self.mock.timeout = -1 self.mock.timeout = -1
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.assertEqual(0, gobject.timeout_add_seconds.call_count) self.assertEqual(0, GObject.timeout_add_seconds.call_count)
self.mock.timeout = None self.mock.timeout = None
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.assertEqual(0, gobject.timeout_add_seconds.call_count) self.assertEqual(0, GObject.timeout_add_seconds.call_count)
def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self): def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self):
self.mock.timeout = 0 self.mock.timeout = 0
@ -313,20 +313,20 @@ class ConnectionTest(unittest.TestCase):
network.Connection.enable_timeout(self.mock) network.Connection.enable_timeout(self.mock)
self.assertEqual(0, self.mock.disable_timeout.call_count) self.assertEqual(0, self.mock.disable_timeout.call_count)
@patch.object(gobject, 'source_remove', new=Mock()) @patch.object(GObject, 'source_remove', new=Mock())
def test_disable_timeout_deregisters(self): def test_disable_timeout_deregisters(self):
self.mock.timeout_id = sentinel.tag self.mock.timeout_id = sentinel.tag
network.Connection.disable_timeout(self.mock) network.Connection.disable_timeout(self.mock)
gobject.source_remove.assert_called_once_with(sentinel.tag) GObject.source_remove.assert_called_once_with(sentinel.tag)
self.assertEqual(None, self.mock.timeout_id) self.assertEqual(None, self.mock.timeout_id)
@patch.object(gobject, 'source_remove', new=Mock()) @patch.object(GObject, 'source_remove', new=Mock())
def test_disable_timeout_already_deregistered(self): def test_disable_timeout_already_deregistered(self):
self.mock.timeout_id = None self.mock.timeout_id = None
network.Connection.disable_timeout(self.mock) network.Connection.disable_timeout(self.mock)
self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(0, GObject.source_remove.call_count)
self.assertEqual(None, self.mock.timeout_id) self.assertEqual(None, self.mock.timeout_id)
def test_queue_send_acquires_and_releases_lock(self): def test_queue_send_acquires_and_releases_lock(self):
@ -372,7 +372,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup(self): def test_recv_callback_respects_io_hup(self):
@ -380,7 +380,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_HUP))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup_and_io_err(self): def test_recv_callback_respects_io_hup_and_io_err(self):
@ -389,7 +389,7 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, self.mock, sentinel.fd,
gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_sends_data_to_actor(self): def test_recv_callback_sends_data_to_actor(self):
@ -398,7 +398,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.actor_ref.tell.assert_called_once_with( self.mock.actor_ref.tell.assert_called_once_with(
{'received': 'data'}) {'received': 'data'})
@ -409,7 +409,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError() self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError()
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_gets_no_data(self): def test_recv_callback_gets_no_data(self):
@ -418,7 +418,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.actor_ref = Mock() self.mock.actor_ref = Mock()
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.assertEqual(self.mock.mock_calls, [ self.assertEqual(self.mock.mock_calls, [
call.sock.recv(any_int), call.sock.recv(any_int),
call.disable_recv(), call.disable_recv(),
@ -431,7 +431,7 @@ class ConnectionTest(unittest.TestCase):
for error in (errno.EWOULDBLOCK, errno.EINTR): for error in (errno.EWOULDBLOCK, errno.EINTR):
self.mock.sock.recv.side_effect = socket.error(error, '') self.mock.sock.recv.side_effect = socket.error(error, '')
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.assertEqual(0, self.mock.stop.call_count) self.assertEqual(0, self.mock.stop.call_count)
def test_recv_callback_unrecoverable_error(self): def test_recv_callback_unrecoverable_error(self):
@ -439,7 +439,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.sock.recv.side_effect = socket.error self.mock.sock.recv.side_effect = socket.error
self.assertTrue(network.Connection.recv_callback( self.assertTrue(network.Connection.recv_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_err(self): def test_send_callback_respects_io_err(self):
@ -450,7 +450,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send_buffer = '' self.mock.send_buffer = ''
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup(self): def test_send_callback_respects_io_hup(self):
@ -461,7 +461,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send_buffer = '' self.mock.send_buffer = ''
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock, sentinel.fd, GObject.IO_IN | GObject.IO_HUP))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup_and_io_err(self): def test_send_callback_respects_io_hup_and_io_err(self):
@ -473,7 +473,7 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, self.mock, sentinel.fd,
gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) GObject.IO_IN | GObject.IO_HUP | GObject.IO_ERR))
self.mock.stop.assert_called_once_with(any_unicode) self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_acquires_and_releases_lock(self): def test_send_callback_acquires_and_releases_lock(self):
@ -484,7 +484,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.sock.send.return_value = 0 self.mock.sock.send.return_value = 0
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.send_lock.acquire.assert_called_once_with(False) self.mock.send_lock.acquire.assert_called_once_with(False)
self.mock.send_lock.release.assert_called_once_with() self.mock.send_lock.release.assert_called_once_with()
@ -496,7 +496,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.sock.send.return_value = 0 self.mock.sock.send.return_value = 0
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.send_lock.acquire.assert_called_once_with(False) self.mock.send_lock.acquire.assert_called_once_with(False)
self.assertEqual(0, self.mock.sock.send.call_count) self.assertEqual(0, self.mock.sock.send.call_count)
@ -507,7 +507,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send.return_value = '' self.mock.send.return_value = ''
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.disable_send.assert_called_once_with() self.mock.disable_send.assert_called_once_with()
self.mock.send.assert_called_once_with('data') self.mock.send.assert_called_once_with('data')
self.assertEqual('', self.mock.send_buffer) self.assertEqual('', self.mock.send_buffer)
@ -519,7 +519,7 @@ class ConnectionTest(unittest.TestCase):
self.mock.send.return_value = 'ta' self.mock.send.return_value = 'ta'
self.assertTrue(network.Connection.send_callback( self.assertTrue(network.Connection.send_callback(
self.mock, sentinel.fd, gobject.IO_IN)) self.mock, sentinel.fd, GObject.IO_IN))
self.mock.send.assert_called_once_with('data') self.mock.send.assert_called_once_with('data')
self.assertEqual('ta', self.mock.send_buffer) self.assertEqual('ta', self.mock.send_buffer)

View File

@ -4,7 +4,7 @@ import errno
import socket import socket
import unittest import unittest
import gobject from gi.repository import GObject
from mock import Mock, patch, sentinel from mock import Mock, patch, sentinel
@ -91,11 +91,11 @@ class ServerTest(unittest.TestCase):
network.Server.create_server_socket( network.Server.create_server_socket(
self.mock, sentinel.host, sentinel.port) self.mock, sentinel.host, sentinel.port)
@patch.object(gobject, 'io_add_watch', new=Mock()) @patch.object(GObject, 'io_add_watch', new=Mock())
def test_register_server_socket_sets_up_io_watch(self): def test_register_server_socket_sets_up_io_watch(self):
network.Server.register_server_socket(self.mock, sentinel.fileno) network.Server.register_server_socket(self.mock, sentinel.fileno)
gobject.io_add_watch.assert_called_once_with( GObject.io_add_watch.assert_called_once_with(
sentinel.fileno, gobject.IO_IN, self.mock.handle_connection) sentinel.fileno, GObject.IO_IN, self.mock.handle_connection)
def test_handle_connection(self): def test_handle_connection(self):
self.mock.accept_connection.return_value = ( self.mock.accept_connection.return_value = (
@ -103,7 +103,7 @@ class ServerTest(unittest.TestCase):
self.mock.maximum_connections_exceeded.return_value = False self.mock.maximum_connections_exceeded.return_value = False
self.assertTrue(network.Server.handle_connection( self.assertTrue(network.Server.handle_connection(
self.mock, sentinel.fileno, gobject.IO_IN)) self.mock, sentinel.fileno, GObject.IO_IN))
self.mock.accept_connection.assert_called_once_with() self.mock.accept_connection.assert_called_once_with()
self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with()
self.mock.init_connection.assert_called_once_with( self.mock.init_connection.assert_called_once_with(
@ -116,7 +116,7 @@ class ServerTest(unittest.TestCase):
self.mock.maximum_connections_exceeded.return_value = True self.mock.maximum_connections_exceeded.return_value = True
self.assertTrue(network.Server.handle_connection( self.assertTrue(network.Server.handle_connection(
self.mock, sentinel.fileno, gobject.IO_IN)) self.mock, sentinel.fileno, GObject.IO_IN))
self.mock.accept_connection.assert_called_once_with() self.mock.accept_connection.assert_called_once_with()
self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with()
self.mock.reject_connection.assert_called_once_with( self.mock.reject_connection.assert_called_once_with(

View File

@ -8,11 +8,8 @@ import mock
import pkg_resources import pkg_resources
import pygst
pygst.require('0.10')
import gst # noqa
from mopidy.internal import deps from mopidy.internal import deps
from mopidy.internal.gi import Gst, gi
class DepsTest(unittest.TestCase): class DepsTest(unittest.TestCase):
@ -74,12 +71,11 @@ class DepsTest(unittest.TestCase):
self.assertEqual('GStreamer', result['name']) self.assertEqual('GStreamer', result['name'])
self.assertEqual( self.assertEqual(
'.'.join(map(str, gst.get_gst_version())), result['version']) '.'.join(map(str, Gst.version())), result['version'])
self.assertIn('gst', result['path']) self.assertIn('gi', result['path'])
self.assertNotIn('__init__.py', result['path']) self.assertNotIn('__init__.py', result['path'])
self.assertIn('Python wrapper: gst-python', result['other']) self.assertIn('Python wrapper: python-gi', result['other'])
self.assertIn( self.assertIn(gi.__version__, result['other'])
'.'.join(map(str, gst.get_pygst_version())), result['other'])
self.assertIn('Relevant elements:', result['other']) self.assertIn('Relevant elements:', result['other'])
@mock.patch('pkg_resources.get_distribution') @mock.patch('pkg_resources.get_distribution')

View File

@ -7,7 +7,7 @@ import shutil
import tempfile import tempfile
import unittest import unittest
import glib from gi.repository import GLib
from mopidy import compat, exceptions from mopidy import compat, exceptions
from mopidy.internal import path from mopidy.internal import path
@ -215,7 +215,7 @@ class ExpandPathTest(unittest.TestCase):
def test_xdg_subsititution(self): def test_xdg_subsititution(self):
self.assertEqual( self.assertEqual(
glib.get_user_data_dir() + b'/foo', GLib.get_user_data_dir() + b'/foo',
path.expand_path(b'$XDG_DATA_DIR/foo')) path.expand_path(b'$XDG_DATA_DIR/foo'))
def test_xdg_subsititution_unknown(self): def test_xdg_subsititution_unknown(self):

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import pytest import pytest
from mopidy import compat
from mopidy.local import translator from mopidy.local import translator
@ -89,7 +90,9 @@ def test_path_to_file_uri(path, uri):
(b'\x00\x01\x02', 'local:track:%00%01%02'), (b'\x00\x01\x02', 'local:track:%00%01%02'),
]) ])
def test_path_to_local_track_uri(path, uri): def test_path_to_local_track_uri(path, uri):
assert translator.path_to_local_track_uri(path) == uri result = translator.path_to_local_track_uri(path)
assert isinstance(result, compat.text_type)
assert result == uri
@pytest.mark.parametrize('path,uri', [ @pytest.mark.parametrize('path,uri', [
@ -99,4 +102,6 @@ def test_path_to_local_track_uri(path, uri):
(b'\x00\x01\x02', 'local:directory:%00%01%02'), (b'\x00\x01\x02', 'local:directory:%00%01%02'),
]) ])
def test_path_to_local_directory_uri(path, uri): def test_path_to_local_directory_uri(path, uri):
assert translator.path_to_local_directory_uri(path) == uri result = translator.path_to_local_directory_uri(path)
assert isinstance(result, compat.text_type)
assert result == uri

View File

@ -80,41 +80,6 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
self.assertTrue(self.core.tracklist.repeat.get()) self.assertTrue(self.core.tracklist.repeat.get())
self.assertInResponse('OK') 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): def test_single_off(self):
self.send_request('single "0"') self.send_request('single "0"')
self.assertFalse(self.core.tracklist.single.get()) self.assertFalse(self.core.tracklist.single.get())
@ -455,9 +420,83 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK') 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 enable_mixer = False
def test_setvol_max_error(self): def test_setvol_without_mixer_fails(self):
self.send_request('setvol "100"') self.send_request('setvol "100"')
self.assertInResponse('ACK [52@0] {setvol} problems setting volume') 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): def test_track_to_mpd_format_with_position_and_tlid(self):
result = translator.track_to_mpd_format( 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(('Pos', 1), result)
self.assertIn(('Id', 2), result) self.assertIn(('Id', 2), result)
@ -153,13 +153,17 @@ class PlaylistMpdFormatTest(unittest.TestCase):
def test_mpd_format(self): def test_mpd_format(self):
playlist = Playlist(tracks=[ 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) result = translator.playlist_to_mpd_format(playlist)
self.assertEqual(len(result), 3) self.assertEqual(len(result), 3)
def test_mpd_format_with_range(self): def test_mpd_format_with_range(self):
playlist = Playlist(tracks=[ 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) result = translator.playlist_to_mpd_format(playlist, 1, 2)
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual(dict(result[0])['Track'], 2) self.assertEqual(dict(result[0])['Track'], 2)