Release v0.16.0

This commit is contained in:
Stein Magnus Jodal 2013-10-27 22:35:44 +01:00
commit 1ff9ce4b08
96 changed files with 3667 additions and 5269 deletions

5
.coveragerc Normal file
View File

@ -0,0 +1,5 @@
[report]
omit =
*/pyshared/*
*/python?.?/*
*/site-packages/nose/*

2
.gitignore vendored
View File

@ -14,3 +14,5 @@ docs/_build/
mopidy.log*
node_modules/
nosetests.xml
*~
*.orig

View File

@ -8,3 +8,5 @@ John Bäckstrand <sopues@gmail.com> <sandos@XBMCLive.(none)>
Alli Witheford <alzeih@gmail.com>
Alexandre Petitjean <alpetitjean@gmail.com>
Alexandre Petitjean <alpetitjean@gmail.com> <alexandre.petitjean@lne.fr>
Javier Domingo Cansino <javier.domingo@fon.com> <javierdo1@gmail.com>
Lasse Bigum <lasse@bigum.org> <l.bigum@samsung.com>

View File

@ -5,14 +5,17 @@ install:
- "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
- "sudo apt-get update || true"
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')"
- "pip install flake8"
- "pip install coveralls flake8"
before_script:
- "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
script:
- "flake8 $(find . -iname '*.py')"
- "nosetests"
- "nosetests --with-coverage --cover-package=mopidy"
after_success:
- "coveralls"
notifications:
irc:

View File

@ -23,4 +23,7 @@
- Tobias Sauerwein <cgtobi@gmail.com>
- Alli Witheford <alzeih@gmail.com>
- Alexandre Petitjean <alpetitjean@gmail.com>
- Terje Larsen <terlar@gmail.com>
- Javier Domingo Cansino <javier.domingo@fon.com>
- Pavol Babincak <scroolik@gmail.com>
- Lasse Bigum <lasse@bigum.org>

View File

@ -2,8 +2,6 @@ include *.rst
include LICENSE
include MANIFEST.in
include data/mopidy.desktop
include mopidy/backends/spotify/spotify_appkey.key
include pylintrc
recursive-include docs *
prune docs/_build

View File

@ -25,5 +25,18 @@ To get started with Mopidy, check out `the docs <http://docs.mopidy.com/>`_.
- Mailing list: `mopidy@googlegroups.com <https://groups.google.com/forum/?fromgroups=#!forum/mopidy>`_
- Twitter: `@mopidy <https://twitter.com/mopidy/>`_
.. image:: https://pypip.in/v/Mopidy/badge.png
:target: https://crate.io/packages/Mopidy/
:alt: Latest PyPI version
.. image:: https://pypip.in/d/Mopidy/badge.png
:target: https://crate.io/packages/Mopidy/
:alt: Number of PyPI downloads
.. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop
:target: https://travis-ci.org/mopidy/mopidy
:alt: Travis CI build status
.. image:: https://coveralls.io/repos/mopidy/mopidy/badge.png?branch=develop
:target: https://coveralls.io/r/mopidy/mopidy?branch=develop
:alt: Test coverage

View File

@ -54,5 +54,4 @@ Backend implementations
* :mod:`mopidy.backends.dummy`
* :mod:`mopidy.backends.local`
* :mod:`mopidy.backends.spotify`
* :mod:`mopidy.backends.stream`

View File

@ -49,5 +49,3 @@ Frontend implementations
* :mod:`mopidy.frontends.http`
* :mod:`mopidy.frontends.mpd`
* :mod:`mopidy.frontends.mpris`
* :mod:`mopidy.frontends.scrobbler`

View File

@ -4,6 +4,159 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v0.16.0 (2013-10-27)
====================
The goals for 0.16 were to add support for queuing playlists of e.g. radio
streams directly to Mopidy, without manually extracting the stream URLs from
the playlist first, and to move the Spotify, Last.fm, and MPRIS support out to
independent Mopidy extensions, living outside the main Mopidy repo. In
addition, we've seen some cleanup to the playback vs tracklist part of the core
API, which will require some changes for users of the HTTP/JavaScript APIs, as
well as the addition of audio muting to the core API. To speed up the
:ref:`development of new extensions <extensiondev>`, we've added a cookiecutter
project to get the skeleton of a Mopidy extension up and running in a matter of
minutes. Read below for all the details and for links to issues with even more
details.
Since the release of 0.15, we've closed or merged 31 issues and pull requests
through about 200 commits by :ref:`five people <authors>`, including three new
contributors.
**Dependencies**
Parts of Mopidy have been moved to their own external extensions. If you want
Mopidy to continue to work like it used to, you may have to install one or more
of the following extensions as well:
- The Spotify backend has been moved to
`Mopidy-Spotify <https://github.com/mopidy/mopidy-spotify>`_.
- The Last.fm scrobbler has been moved to
`Mopidy-Scrobbler <https://github.com/mopidy/mopidy-scrobbler>`_.
- The MPRIS frontend has been moved to
`Mopidy-MPRIS <https://github.com/mopidy/mopidy-mpris>`_.
**Core**
- Parts of the functionality in :class:`mopidy.core.PlaybackController` have
been moved to :class:`mopidy.core.TracklistController`:
=================================== ==================================
Old location New location
=================================== ==================================
playback.get_consume() tracklist.get_consume()
playback.set_consume(v) tracklist.set_consume(v)
playback.consume tracklist.consume
playback.get_random() tracklist.get_random()
playback.set_random(v) tracklist.set_random(v)
playback.random tracklist.random
playback.get_repeat() tracklist.get_repeat()
playback.set_repeat(v) tracklist.set_repeat(v)
playback.repeat tracklist.repeat
playback.get_single() tracklist.get_single()
playback.set_single(v) tracklist.set_single(v)
playback.single tracklist.single
playback.get_tracklist_position() tracklist.index(tl_track)
playback.tracklist_position tracklist.index(tl_track)
playback.get_tl_track_at_eot() tracklist.eot_track(tl_track)
playback.tl_track_at_eot tracklist.eot_track(tl_track)
playback.get_tl_track_at_next() tracklist.next_track(tl_track)
playback.tl_track_at_next tracklist.next_track(tl_track)
playback.get_tl_track_at_previous() tracklist.previous_track(tl_track)
playback.tl_track_at_previous tracklist.previous_track(tl_track)
=================================== ==================================
The ``tl_track`` argument to the last four new functions are used as the
reference ``tl_track`` in the tracklist to find e.g. the next track. Usually,
this will be :attr:`~mopidy.core.PlaybackController.current_tl_track`.
- Added :attr:`mopidy.core.PlaybackController.mute` for muting and unmuting
audio. (Fixes: :issue:`186`)
- Added :meth:`mopidy.core.CoreListener.mute_changed` event that is triggered
when the mute state changes.
- In "random" mode, after a full playthrough of the tracklist, playback
continued from the last track played to the end of the playlist in non-random
order. It now stops when all tracks have been played once, unless "repeat"
mode is enabled. (Fixes: :issue:`453`)
- In "single" mode, after a track ended, playback continued with the next track
in the tracklist. It now stops after playing a single track, unless "repeat"
mode is enabled. (Fixes: :issue:`496`)
**Audio**
- Added support for parsing and playback of playlists in GStreamer. For end
users this basically means that you can now add a radio playlist to Mopidy
and we will automatically download it and play the stream inside it.
Currently we support M3U, PLS, XSPF and ASX files. Also note that we can
currently only play the first stream in the playlist.
- We now handle the rare case where an audio track has max volume equal to min.
This was causing divide by zero errors when scaling volumes to a zero to
hundred scale. (Fixes: :issue:`525`)
- Added support for muting audio without setting the volume to 0. This works
both for the software and hardware mixers. (Fixes: :issue:`186`)
**Local backend**
- Replaced our custom media library scanner with GStreamer's builtin scanner.
This should make scanning less error prone and faster as timeouts should be
infrequent. (Fixes: :issue:`198`)
- Media files with less than 100ms duration are now excluded from the library.
- Media files with the file extensions ``.jpeg``, ``.jpg``, ``.png``, ``.txt``,
and ``.log`` are now skipped by the media library scanner. You can change the
list of excluded file extensions by setting the
:confval:`local/excluded_file_extensions` config value. (Fixes: :issue:`516`)
- Unknown URIs found in playlists are now made into track objects with the URI
set instead of being ignored. This makes it possible to have playlists with
e.g. HTTP radio streams and not just ``local:track:...`` URIs. This used to
work, but was broken in Mopidy 0.15.0. (Fixes: :issue:`527`)
- Fixed crash when playing ``local:track:...`` URIs which contained non-ASCII
chars after uridecode.
- Removed media files are now also removed from the in-memory media library
when the media library is reloaded from disk. (Fixes: :issue:`500`)
**MPD frontend**
- Made the formerly unused commands ``outputs``, ``enableoutput``, and
``disableoutput`` mute/unmute audio. (Related to: :issue:`186`)
- The MPD command ``list`` now works with ``"albumartist"`` as its second
argument, e.g. ``list "album" "albumartist" "anartist"``. (Fixes:
:issue:`468`)
- The MPD commands ``find`` and ``search`` now accepts ``albumartist`` and
``track`` (this is the track number, not the track name) as field types to
limit the search result with.
- The MPD command ``count`` is now implemented. It accepts the same type of
arguments as ``find`` and ``search``, but returns the number of tracks and
their total playtime instead.
**Extension support**
- A cookiecutter project for quickly creating new Mopidy extensions have been
created. You can find it at `cookiecutter-mopidy-ext
<https://github.com/mopidy/cookiecutter-mopidy-ext>`_. (Fixes: :issue:`522`)
v0.15.0 (2013-09-19)
====================

View File

@ -8,7 +8,8 @@ MPRIS clients
Specification. It's a spec that describes a standard D-Bus interface for making
media players available to other applications on the same system.
Mopidy's :ref:`MPRIS frontend <ext-mpris>` currently implements all required
The MPRIS frontend provided by the `Mopidy-MPRIS extension
<https://github.com/mopidy/mopidy-mpris>`_ currently implements all required
parts of the MPRIS spec, plus the optional playlist interface. It does not
implement the optional tracklist interface.

View File

@ -36,19 +36,21 @@ How to make Mopidy available as an UPnP MediaRenderer
=====================================================
With the help of `the Rygel project <https://live.gnome.org/Rygel>`_ Mopidy can
be made available as an UPnP MediaRenderer. Rygel will interface with Mopidy's
:ref:`MPRIS frontend <ext-mpris>`, and make Mopidy available as a MediaRenderer
on the local network. Since this depends on the MPRIS frontend, which again
depends on D-Bus being available, this will only work on Linux, and not OS X.
MPRIS/D-Bus is only available to other applications on the same host, so Rygel
must be running on the same machine as Mopidy.
be made available as an UPnP MediaRenderer. Rygel will interface with the MPRIS
interface provided by the `Mopidy-MPRIS extension
<https://github.com/mopidy/mopidy-mpris>`_, and make Mopidy available as a
MediaRenderer on the local network. Since this depends on the MPRIS frontend,
which again depends on D-Bus being available, this will only work on Linux, and
not OS X. MPRIS/D-Bus is only available to other applications on the same
host, so Rygel must be running on the same machine as Mopidy.
1. Start Mopidy and make sure the :ref:`MPRIS frontend <ext-mpris>` is working.
It is activated by default, but you may miss dependencies or be using OS X,
in which case it will not work. Check the console output when Mopidy is
started for any errors related to the MPRIS frontend. If you're unsure it is
working, there are instructions for how to test it on the :ref:`MPRIS
frontend <ext-mpris>` page.
1. Start Mopidy and make sure the MPRIS frontend is working. It is activated
by default when the Mopidy-MPRIS extension is installed, but you may miss
dependencies or be using OS X, in which case it will not work. Check the
console output when Mopidy is started for any errors related to the MPRIS
frontend. If you're unsure it is working, there are instructions for how to
test it on in the `Mopidy-MPRIS readme
<https://github.com/mopidy/mopidy-mpris>`_.
2. Install Rygel. On Debian/Ubuntu::

View File

@ -35,6 +35,8 @@ class Mock(object):
elif (name[0] == name[0].upper()
# gst.interfaces.MIXER_TRACK_*
and not name.startswith('MIXER_TRACK_')
# gst.PadTemplate
and not name.startswith('PadTemplate')
# dbus.String()
and not name == 'String'):
return type(name, (), {})
@ -76,6 +78,9 @@ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + '/../'))
# the string True.
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# Enable Read the Docs' new theme
RTD_NEW_THEME = True
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be

View File

@ -147,24 +147,6 @@ Core configuration values
.. _the Python logging docs: http://docs.python.org/2/library/logging.config.html
Extension configuration
=======================
Mopidy's extensions have their own config values that you may want to tweak.
For the available config values, please refer to the docs for each extension.
Most, if not all, can be found at :ref:`ext`.
Mopidy extensions are enabled by default when they are installed. If you want
to disable an extension without uninstalling it, all extensions support the
``enabled`` config value even if it isn't explicitly documented by all
extensions. If the ``enabled`` config value is set to ``false`` the extension
will not be started. For example, to disable the Spotify extension, add the
following to your ``mopidy.conf``::
[spotify]
enabled = false
Extension configuration
=======================
@ -227,6 +209,13 @@ this work first::
Streaming through SHOUTcast/Icecast
-----------------------------------
.. warning:: Known issue
Currently, Mopidy does not handle end-of-track vs end-of-stream signalling
in GStreamer correctly. This causes the SHOUTcast stream to be disconnected
at the end of each track, rendering it quite useless. For further details,
see :issue:`492`.
If you want to play the audio on another computer than the one running Mopidy,
you can stream the audio from Mopidy through an SHOUTcast or Icecast audio
streaming server. Multiple media players can then be connected to the streaming

View File

@ -30,96 +30,107 @@ These extensions are maintained outside Mopidy's core, often by other
developers.
Mopidy-Arcam
------------
https://github.com/TooDizzy/mopidy-arcam
Extension for controlling volume using an external Arcam amplifier. Developed
and tested with an Arcam AVR-300.
Mopidy-Beets
------------
https://github.com/mopidy/mopidy-beets
Provides a backend for playing music from your `Beets
<http://beets.radbox.org/>`_ music library through Beets' web extension.
Author:
Janez Troha
PyPI:
`Mopidy-Beets <https://pypi.python.org/pypi/Mopidy-Beets>`_
GitHub:
`dz0ny/mopidy-beets <https://github.com/dz0ny/mopidy-beets>`_
Issues:
https://github.com/dz0ny/mopidy-beets/issues
Mopidy-GMusic
-------------
https://github.com/hechtus/mopidy-gmusic
Provides a backend for playing music from `Google Play Music
<https://play.google.com/music/>`_.
Author:
Ronald Hecht
PyPI:
`Mopidy-GMusic <https://pypi.python.org/pypi/Mopidy-GMusic>`_
GitHub:
`hechtus/mopidy-gmusic <https://github.com/hechtus/mopidy-gmusic>`_
Issues:
https://github.com/hechtus/mopidy-gmusic/issues
Mopidy-MPRIS
------------
https://github.com/mopidy/mopidy-mpris
Extension for controlling Mopidy through the `MPRIS <http://www.mpris.org/>`_
D-Bus interface, for example using the Ubuntu Sound Menu.
Mopidy-NAD
----------
https://github.com/mopidy/mopidy-nad
Extension for controlling volume using an external NAD amplifier.
Author:
Stein Magnus Jodal
PyPI:
`Mopidy-NAD <https://pypi.python.org/pypi/Mopidy-NAD>`_
GitHub:
`mopidy/mopidy-nad <https://github.com/mopidy/mopidy-nad>`_
Issues:
https://github.com/mopidy/mopidy/issues
Mopidy-Notifier
---------------
https://github.com/sauberfred/mopidy-notifier
Extension for displaying track info as User Notifications in Mac OS X.
Mopidy-radio-de
---------------
https://github.com/hechtus/mopidy-radio-de
Extension for listening to Internet radio stations and podcasts listed at
`radio.de <http://www.radio.de/>`_, `rad.io <http://www.rad.io/>`_,
`radio.fr <http://www.radio.fr/>`_, and `radio.at <http://www.radio.at/>`_.
Mopidy-Scrobbler
----------------
https://github.com/mopidy/mopidy-scrobbler
Extension for scrobbling played tracks to Last.fm.
Mopidy-SomaFM
-------------
https://github.com/AlexandrePTJ/mopidy-somafm
Provides a backend for playing music from the `SomaFM <http://somafm.com/>`_
service.
Author:
Alexandre Petitjean
PyPI:
`Mopidy-SomaFM <https://pypi.python.org/pypi/Mopidy-SomaFM>`_
GitHub:
`AlexandrePTJ/mopidy-somafm <https://github.com/AlexandrePTJ/mopidy-somafm/>`_
Issues:
https://github.com/AlexandrePTJ/mopidy-somafm/issues
Mopidy-SoundCloud
-----------------
https://github.com/mopidy/mopidy-soundcloud
Provides a backend for playing music from the `SoundCloud
<http://www.soundcloud.com/>`_ service.
Author:
Janez Troha
PyPI:
`Mopidy-SoundCloud <https://pypi.python.org/pypi/Mopidy-SoundCloud>`_
GitHub:
`dz0ny/mopidy-soundcloud <https://github.com/dz0ny/mopidy-soundcloud>`_
Issues:
https://github.com/dz0ny/mopidy-soundcloud/issues
Mopidy-Spotify
--------------
https://github.com/mopidy/mopidy-spotify
Extension for playing music from the `Spotify <http://www.spotify.com/>`_ music
streaming service.
Mopidy-Subsonic
---------------
https://github.com/rattboi/mopidy-subsonic
Provides a backend for playing music from a `Subsonic Music Streamer
<http://www.subsonic.org/>`_ library.
Author:
Bradon Kanyid
PyPI:
`Mopidy-Subsonic <https://pypi.python.org/pypi/Mopidy-Subsonic>`_
GitHub:
`rattboi/mopidy-subsonic <https://github.com/rattboi/mopidy-subsonic>`_
Issues:
https://github.com/rattboi/mopidy-subsonic/issues

View File

@ -52,6 +52,10 @@ Configuration values
Number of milliseconds before giving up scanning a file and moving on to
the next file.
.. confval:: local/excluded_file_extensions
File extensions to exclude when scanning the media directory.
Usage
=====

View File

@ -1,105 +0,0 @@
.. _ext-mpris:
************
Mopidy-MPRIS
************
This extension lets you control Mopidy through the Media Player Remote
Interfacing Specification (`MPRIS <http://www.mpris.org/>`_) D-Bus interface.
An example of an MPRIS client is the :ref:`ubuntu-sound-menu`.
Dependencies
============
- D-Bus Python bindings. The package is named ``python-dbus`` in
Ubuntu/Debian.
- ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the
Ubuntu Sound Menu. The package is named ``python-indicate`` in
Ubuntu/Debian.
- An ``.desktop`` file for Mopidy installed at the path set in the
:confval:`mpris/desktop_file` config value. See usage section below for
details.
Default configuration
=====================
.. literalinclude:: ../../mopidy/frontends/mpris/ext.conf
:language: ini
Configuration values
====================
.. confval:: mpris/enabled
If the MPRIS extension should be enabled or not.
.. confval:: mpris/desktop_file
Location of the Mopidy ``.desktop`` file.
Usage
=====
The extension is enabled by default if all dependencies are available.
Controlling Mopidy through the Ubuntu Sound Menu
------------------------------------------------
If you are running Ubuntu and installed Mopidy using the Debian package from
APT you should be able to control Mopidy through the :ref:`ubuntu-sound-menu`
without any changes.
If you installed Mopidy in any other way and want to control Mopidy through the
Ubuntu Sound Menu, you must install the ``mopidy.desktop`` file which can be
found in the ``data/`` dir of the Mopidy source repo into the
``/usr/share/applications`` dir by hand::
cd /path/to/mopidy/source
sudo cp data/mopidy.desktop /usr/share/applications/
If the correct path to the installed ``mopidy.desktop`` file on your system
isn't ``/usr/share/applications/mopidy.conf``, you'll need to set the
:confval:`mpris/desktop_file` config value.
After you have installed the file, start Mopidy in any way, and Mopidy should
appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed
in the Ubuntu Sound Menu, and may be restarted by selecting it there.
The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend. The MPRIS
frontend supports the minimum requirements of the `MPRIS specification
<http://www.mpris.org/>`_. The ``TrackList`` interface of the spec is not
supported.
Testing the MPRIS API directly
------------------------------
To use the MPRIS API directly, start Mopidy, and then run the following in a
Python shell::
import dbus
bus = dbus.SessionBus()
player = bus.get_object('org.mpris.MediaPlayer2.mopidy',
'/org/mpris/MediaPlayer2')
Now you can control Mopidy through the player object. Examples:
- To get some properties from Mopidy, run::
props = player.GetAll('org.mpris.MediaPlayer2',
dbus_interface='org.freedesktop.DBus.Properties')
- To quit Mopidy through D-Bus, run::
player.Quit(dbus_interface='org.mpris.MediaPlayer2')
For details on the API, please refer to the `MPRIS specification
<http://www.mpris.org/>`_.

View File

@ -1,55 +0,0 @@
.. _ext-scrobbler:
****************
Mopidy-Scrobbler
****************
This extension scrobbles the music you play to your `Last.fm
<http://www.last.fm>`_ profile.
.. note::
This extension requires a free user account at Last.fm.
Dependencies
============
.. literalinclude:: ../../requirements/scrobbler.txt
Default configuration
=====================
.. literalinclude:: ../../mopidy/frontends/scrobbler/ext.conf
:language: ini
Configuration values
====================
.. confval:: scrobbler/enabled
If the scrobbler extension should be enabled or not.
.. confval:: scrobbler/username
Your Last.fm username.
.. confval:: scrobbler/password
Your Last.fm password.
Usage
=====
The extension is enabled by default if all dependencies are available. You just
need to add your Last.fm username and password to the
``~/.config/mopidy/mopidy.conf`` file:
.. code-block:: ini
[scrobbler]
username = myusername
password = mysecret

View File

@ -1,83 +0,0 @@
.. _ext-spotify:
**************
Mopidy-Spotify
**************
An extension for playing music from Spotify.
`Spotify <http://www.spotify.com/>`_ is a music streaming service. The backend
uses the official `libspotify
<http://developer.spotify.com/en/libspotify/overview/>`_ library and the
`pyspotify <http://github.com/mopidy/pyspotify/>`_ Python bindings for
libspotify. This backend handles URIs starting with ``spotify:``.
.. note::
This product uses SPOTIFY(R) CORE but is not endorsed, certified or
otherwise approved in any way by Spotify. Spotify is the registered
trade mark of the Spotify Group.
Known issues
============
https://github.com/mopidy/mopidy/issues?labels=Spotify+backend
Dependencies
============
.. literalinclude:: ../../requirements/spotify.txt
Default configuration
=====================
.. literalinclude:: ../../mopidy/backends/spotify/ext.conf
:language: ini
Configuration values
====================
.. confval:: spotify/enabled
If the Spotify extension should be enabled or not.
.. confval:: spotify/username
Your Spotify Premium username.
.. confval:: spotify/password
Your Spotify Premium password.
.. confval:: spotify/bitrate
The preferred audio bitrate. Valid values are 96, 160, 320.
.. confval:: spotify/timeout
Max number of seconds to wait for Spotify operations to complete.
.. confval:: spotify/cache_dir
Path to the Spotify data cache. Cannot be shared with other Spotify apps.
Usage
=====
If you are using the Spotify backend, which is the default, enter your Spotify
Premium account's username and password into ``~/.config/mopidy/mopidy.conf``,
like this:
.. code-block:: ini
[spotify]
username = myusername
password = mysecret
This will only work if you have the Spotify Premium subscription. Spotify
Unlimited will not work.

View File

@ -62,6 +62,20 @@ extension, Mopidy-Soundspot::
Example content for the most important files follows below.
cookiecutter project template
=============================
We've also made a `cookiecutter <http://cookiecutter.readthedocs.org/>`_
project template for `creating new Mopidy extensions
<https://github.com/mopidy/cookiecutter-mopidy-ext>`_. If you install
cookiecutter and run a single command, you're asked a few questions about the
name of your extension, etc. This is used to create a folder structure similar
to the above, with all the needed files and most of the details filled in for
you. This saves you a lot of tedious work and copy-pasting from this howto. See
the readme of `cookiecutter-mopidy-ext
<https://github.com/mopidy/cookiecutter-mopidy-ext>`_ for further details.
Example README.rst
==================
@ -73,24 +87,30 @@ installation using ``pip install Mopidy-Something==dev`` to work.
.. code-block:: rst
****************
Mopidy-Soundspot
================
****************
`Mopidy <http://www.mopidy.com/>`_ extension for playing music from
`Soundspot <http://soundspot.example.com/>`_.
Usage
-----
Requires a Soundspot Platina subscription and the pysoundspot library.
Installation
============
Install by running::
sudo pip install Mopidy-Soundspot
Or install the Debian/Ubuntu package from `apt.mopidy.com
Or, if available, install the Debian/Ubuntu package from `apt.mopidy.com
<http://apt.mopidy.com/>`_.
Configuration
=============
Before starting Mopidy, you must add your Soundspot username and password
to the Mopidy configuration file::
@ -98,34 +118,46 @@ installation using ``pip install Mopidy-Something==dev`` to work.
username = alice
password = secret
Project resources
-----------------
=================
- `Source code <https://github.com/mopidy/mopidy-soundspot>`_
- `Issue tracker <https://github.com/mopidy/mopidy-soundspot/issues>`_
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/develop#egg=Mopidy-Soundspot-dev>`_
- `Download development snapshot <https://github.com/mopidy/mopidy-soundspot/tarball/master#egg=Mopidy-Soundspot-dev>`_
Changelog
=========
v0.1.0 (2013-09-17)
-------------------
- Initial release.
Example setup.py
================
The ``setup.py`` file must use setuptools/distribute, and not distutils. This
is because Mopidy extensions use setuptools' entry point functionality to
register themselves as available Mopidy extensions when they are installed on
your system.
The ``setup.py`` file must use setuptools, and not distutils. This is because
Mopidy extensions use setuptools' entry point functionality to register
themselves as available Mopidy extensions when they are installed on your
system.
The example below also includes a couple of convenient tricks for reading the
package version from the source code so that it is defined in a single place,
and to reuse the README file as the long description of the package for the
PyPI registration.
The package must have ``install_requires`` on ``setuptools`` and ``Mopidy``, in
addition to any other dependencies required by your extension. The
``entry_points`` part must be included. The ``mopidy.ext`` part cannot be
changed, but the innermost string should be changed. It's format is
``ext_name = package_name:Extension``. ``ext_name`` should be a short
name for your extension, typically the part after "Mopidy-" in lowercase. This
name is used e.g. to name the config section for your extension. The
The package must have ``install_requires`` on ``setuptools`` and ``Mopidy >=
0.14`` (or a newer version, if your extension requires it), in addition to any
other dependencies required by your extension. If you implement a Mopidy
frontend or backend, you'll need to include ``Pykka >= 1.1`` in the
requirements. The ``entry_points`` part must be included. The ``mopidy.ext``
part cannot be changed, but the innermost string should be changed. It's format
is ``ext_name = package_name:Extension``. ``ext_name`` should be a short name
for your extension, typically the part after "Mopidy-" in lowercase. This name
is used e.g. to name the config section for your extension. The
``package_name:Extension`` part is simply the Python path to the extension
class that will connect the rest of the dots.
@ -134,7 +166,7 @@ class that will connect the rest of the dots.
from __future__ import unicode_literals
import re
from setuptools import setup
from setuptools import setup, find_packages
def get_version(filename):
@ -146,20 +178,26 @@ class that will connect the rest of the dots.
setup(
name='Mopidy-Soundspot',
version=get_version('mopidy_soundspot/__init__.py'),
url='http://example.com/mopidy-soundspot/',
url='https://github.com/your-account/mopidy-soundspot',
license='Apache License, Version 2.0',
author='Your Name',
author_email='your-email@example.com',
description='Very short description',
long_description=open('README.rst').read(),
packages=['mopidy_soundspot'],
packages=find_packages(exclude=['tests', 'tests.*']),
zip_safe=False,
include_package_data=True,
install_requires=[
'setuptools',
'Mopidy',
'Mopidy >= 0.14',
'Pykka >= 1.1',
'pysoundspot',
],
test_suite='nose.collector',
tests_require=[
'nose',
'mock >= 1.0',
],
entry_points={
'mopidy.ext': [
'soundspot = mopidy_soundspot:Extension',

View File

@ -24,10 +24,9 @@ Glossary
frontend
A part of Mopidy *using* the :term:`core` API. Existing frontends
include the :ref:`MPD server <ext-mpd>`, the :ref:`MPRIS/D-Bus
integration <ext-mpris>`, the :ref:`Last.fm scrobbler <ext-scrobbler>`,
and the :ref:`HTTP server <ext-http>` with JavaScript API. See
:ref:`frontend-api` for details.
include the :ref:`MPD server <ext-mpd>`, the MPRIS/D-Bus integration,
the Last.fm scrobbler, and the :ref:`HTTP server <ext-http>` with
JavaScript API. See :ref:`frontend-api` for details.
mixer
A GStreamer element that controls audio volume.

View File

@ -4,9 +4,9 @@ Mopidy
Mopidy is a music server which can play music both from multiple sources, like
your :ref:`local hard drive <ext-local>`, :ref:`radio streams <ext-stream>`,
and from :ref:`Spotify <ext-spotify>` and SoundCloud. Searches combines results
from all music sources, and you can mix tracks from all sources in your play
queue. Your playlists from Spotify or SoundCloud are also available for use.
and from Spotify and SoundCloud. Searches combines results from all music
sources, and you can mix tracks from all sources in your play queue. Your
playlists from Spotify or SoundCloud are also available for use.
To control your Mopidy music server, you can use one of Mopidy's :ref:`web
clients <http-clients>`, the :ref:`Ubuntu Sound Menu <ubuntu-sound-menu>`, any
@ -30,7 +30,7 @@ Usage
=====
.. toctree::
:maxdepth: 3
:maxdepth: 2
installation/index
installation/raspberrypi
@ -81,4 +81,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -87,8 +87,9 @@ Mopidy Git repo, which always corresponds to the latest release.
To upgrade Mopidy to future releases, just rerun ``makepkg``.
#. Optional: If you want to scrobble your played tracks to Last.fm, you need to
install `python2-pylast
<https://aur.archlinux.org/packages/python2-pylast/>`_ from AUR.
install `python2-pylast`::
sudo pacman -S python2-pylast
#. Finally, you need to set a couple of :doc:`config values </config>`, and
then you're ready to :doc:`run Mopidy </running>`.
@ -174,10 +175,10 @@ can install Mopidy from PyPI using Pip.
#. Then you'll need to install all of Mopidy's hard non-Python dependencies:
- GStreamer 0.10.x, with Python bindings. GStreamer is packaged for most
popular Linux distributions. Search for GStreamer in your package manager,
and make sure to install the Python bindings, and the "good" and "ugly"
plugin sets.
- GStreamer 0.10 (>= 0.10.31, < 0.11), with Python bindings. GStreamer is
packaged for most popular Linux distributions. Search for GStreamer in
your package manager, and make sure to install the Python bindings, and
the "good" and "ugly" plugin sets.
If you use Debian/Ubuntu you can install GStreamer like this::
@ -250,8 +251,8 @@ can install Mopidy from PyPI using Pip.
sudo pip-python install -U cherrypy ws4py
#. Optional: To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound
Menu or from an UPnP client via Rygel, you need some additional
#. Optional: To use Mopidy-MPRIS, e.g. for controlling Mopidy from the Ubuntu
Sound Menu or from an UPnP client via Rygel, you need some additional
dependencies: the Python bindings for libindicate, and the Python bindings
for libdbus, the reference D-Bus library.

View File

@ -21,4 +21,4 @@ if (isinstance(pykka.__version__, basestring)
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '0.15.0'
__version__ = '0.16.0'

View File

@ -11,7 +11,7 @@ import pykka
from mopidy.utils import process
from . import mixers, utils
from . import mixers, playlists, utils
from .constants import PlaybackState
from .listener import AudioListener
@ -19,6 +19,9 @@ logger = logging.getLogger('mopidy.audio')
mixers.register_mixers()
playlists.register_typefinders()
playlists.register_elements()
MB = 1 << 20
@ -541,9 +544,42 @@ class Audio(pykka.ThreadingActor):
"""Convert value between scales."""
new_min, new_max = new
old_min, old_max = old
if old_min == old_max:
return old_max
scaling = float(new_max - new_min) / (old_max - old_min)
return int(round(scaling * (value - old_min) + new_min))
def get_mute(self):
"""
Get mute status of the installed mixer.
:rtype: :class:`True` if muted, :class:`False` if unmuted,
:class:`None` if no mixer is installed.
"""
if self._software_mixing:
return self._playbin.get_property('mute')
if self._mixer_track is None:
return None
return bool(self._mixer_track.flags & gst.interfaces.MIXER_TRACK_MUTE)
def set_mute(self, mute):
"""
Mute or unmute of the installed mixer.
:param mute: Wether to mute the mixer or not.
:type mute: bool
:rtype: :class:`True` if successful, else :class:`False`
"""
if self._software_mixing:
return self._playbin.set_property('mute', bool(mute))
if self._mixer_track is None:
return False
return self._mixer.set_mute(self._mixer_track, bool(mute))
def set_metadata(self, track):
"""
Set track metadata for currently playing song.

412
mopidy/audio/playlists.py Normal file
View File

@ -0,0 +1,412 @@
from __future__ import unicode_literals
import pygst
pygst.require('0.10')
import gst
import gobject
import ConfigParser as configparser
import io
try:
import xml.etree.cElementTree as elementtree
except ImportError:
import xml.etree.ElementTree as elementtree
# TODO: make detect_FOO_header reusable in general mopidy code.
# i.e. give it just a "peek" like function.
def detect_m3u_header(typefind):
return typefind.peek(0, 8) == b'#EXTM3U\n'
def detect_pls_header(typefind):
return typefind.peek(0, 11).lower() == b'[playlist]\n'
def detect_xspf_header(typefind):
data = typefind.peek(0, 150)
if b'xspf' not in data:
return False
try:
data = io.BytesIO(data)
for event, element in elementtree.iterparse(data, events=(b'start',)):
return element.tag.lower() == '{http://xspf.org/ns/0/}playlist'
except elementtree.ParseError:
pass
return False
def detect_asx_header(typefind):
data = typefind.peek(0, 50)
if b'asx' not in data:
return False
try:
data = io.BytesIO(data)
for event, element in elementtree.iterparse(data, events=(b'start',)):
return element.tag.lower() == 'asx'
except elementtree.ParseError:
pass
return False
def parse_m3u(data):
# TODO: convert non URIs to file URIs.
found_header = False
for line in data.readlines():
if found_header or line.startswith('#EXTM3U'):
found_header = True
else:
continue
if not line.startswith('#') and line.strip():
yield line.strip()
def parse_pls(data):
# TODO: convert non URIs to file URIs.
try:
cp = configparser.RawConfigParser()
cp.readfp(data)
except configparser.Error:
return
for section in cp.sections():
if section.lower() != 'playlist':
continue
for i in xrange(cp.getint(section, 'numberofentries')):
yield cp.get(section, 'file%d' % (i+1))
def parse_xspf(data):
try:
for event, element in elementtree.iterparse(data):
element.tag = element.tag.lower() # normalize
except elementtree.ParseError:
return
ns = 'http://xspf.org/ns/0/'
for track in element.iterfind('{%s}tracklist/{%s}track' % (ns, ns)):
yield track.findtext('{%s}location' % ns)
def parse_asx(data):
try:
for event, element in elementtree.iterparse(data):
element.tag = element.tag.lower() # normalize
except elementtree.ParseError:
return
for ref in element.findall('entry/ref'):
yield ref.get('href', '').strip()
def parse_urilist(data):
for line in data.readlines():
if not line.startswith('#') and gst.uri_is_valid(line.strip()):
yield line
def playlist_typefinder(typefind, func, caps):
if func(typefind):
typefind.suggest(gst.TYPE_FIND_MAXIMUM, caps)
def register_typefind(mimetype, func, extensions):
caps = gst.caps_from_string(mimetype)
gst.type_find_register(mimetype, gst.RANK_PRIMARY, playlist_typefinder,
extensions, caps, func, caps)
def register_typefinders():
register_typefind('audio/x-mpegurl', detect_m3u_header, [b'm3u', b'm3u8'])
register_typefind('audio/x-scpls', detect_pls_header, [b'pls'])
register_typefind('application/xspf+xml', detect_xspf_header, [b'xspf'])
# NOTE: seems we can't use video/x-ms-asf which is the correct mime for asx
# as it is shared with asf for streaming videos :/
register_typefind('audio/x-ms-asx', detect_asx_header, [b'asx'])
class BasePlaylistElement(gst.Bin):
"""Base class for creating GStreamer elements for playlist support.
This element performs the following steps:
1. Initializes src and sink pads for the element.
2. Collects data from the sink until EOS is reached.
3. Passes the collected data to :meth:`convert` to get a list of URIs.
4. Passes the list of URIs to :meth:`handle`, default handling is to pass
the URIs to the src element as a uri-list.
5. If handle returned true, the EOS consumed and nothing more happens, if
it is not consumed it flows on to the next element downstream, which is
likely our uri-list consumer which needs the EOS to know we are done
sending URIs.
"""
sinkpad_template = None
"""GStreamer pad template to use for sink, must be overriden."""
srcpad_template = None
"""GStreamer pad template to use for src, must be overriden."""
ghost_srcpad = False
"""Indicates if src pad should be ghosted or not."""
def __init__(self):
"""Sets up src and sink pads plus behaviour."""
super(BasePlaylistElement, self).__init__()
self._data = io.BytesIO()
self._done = False
self.sinkpad = gst.Pad(self.sinkpad_template)
self.sinkpad.set_chain_function(self._chain)
self.sinkpad.set_event_function(self._event)
self.add_pad(self.sinkpad)
if self.ghost_srcpad:
self.srcpad = gst.ghost_pad_new_notarget('src', gst.PAD_SRC)
else:
self.srcpad = gst.Pad(self.srcpad_template)
self.add_pad(self.srcpad)
def convert(self, data):
"""Convert the data we have colleted to URIs.
:param data: collected data buffer
:type data: :class:`io.BytesIO`
:returns: iterable or generator of URIs
"""
raise NotImplementedError
def handle(self, uris):
"""Do something useful with the URIs.
:param uris: list of URIs
:type uris: :type:`list`
:returns: boolean indicating if EOS should be consumed
"""
# TODO: handle unicode uris which we can get out of elementtree
self.srcpad.push(gst.Buffer('\n'.join(uris)))
return False
def _chain(self, pad, buf):
if not self._done:
self._data.write(buf.data)
return gst.FLOW_OK
return gst.FLOW_EOS
def _event(self, pad, event):
if event.type == gst.EVENT_NEWSEGMENT:
return True
if event.type == gst.EVENT_EOS:
self._done = True
self._data.seek(0)
if self.handle(list(self.convert(self._data))):
return True
# Ensure we handle remaining events in a sane way.
return pad.event_default(event)
class M3uDecoder(BasePlaylistElement):
__gstdetails__ = ('M3U Decoder',
'Decoder',
'Convert .m3u to text/uri-list',
'Mopidy')
sinkpad_template = gst.PadTemplate(
'sink', gst.PAD_SINK, gst.PAD_ALWAYS,
gst.caps_from_string('audio/x-mpegurl'))
srcpad_template = gst.PadTemplate(
'src', gst.PAD_SRC, gst.PAD_ALWAYS,
gst.caps_from_string('text/uri-list'))
__gsttemplates__ = (sinkpad_template, srcpad_template)
def convert(self, data):
return parse_m3u(data)
class PlsDecoder(BasePlaylistElement):
__gstdetails__ = ('PLS Decoder',
'Decoder',
'Convert .pls to text/uri-list',
'Mopidy')
sinkpad_template = gst.PadTemplate(
'sink', gst.PAD_SINK, gst.PAD_ALWAYS,
gst.caps_from_string('audio/x-scpls'))
srcpad_template = gst.PadTemplate(
'src', gst.PAD_SRC, gst.PAD_ALWAYS,
gst.caps_from_string('text/uri-list'))
__gsttemplates__ = (sinkpad_template, srcpad_template)
def convert(self, data):
return parse_pls(data)
class XspfDecoder(BasePlaylistElement):
__gstdetails__ = ('XSPF Decoder',
'Decoder',
'Convert .pls to text/uri-list',
'Mopidy')
sinkpad_template = gst.PadTemplate(
'sink', gst.PAD_SINK, gst.PAD_ALWAYS,
gst.caps_from_string('application/xspf+xml'))
srcpad_template = gst.PadTemplate(
'src', gst.PAD_SRC, gst.PAD_ALWAYS,
gst.caps_from_string('text/uri-list'))
__gsttemplates__ = (sinkpad_template, srcpad_template)
def convert(self, data):
return parse_xspf(data)
class AsxDecoder(BasePlaylistElement):
__gstdetails__ = ('ASX Decoder',
'Decoder',
'Convert .asx to text/uri-list',
'Mopidy')
sinkpad_template = gst.PadTemplate(
'sink', gst.PAD_SINK, gst.PAD_ALWAYS,
gst.caps_from_string('audio/x-ms-asx'))
srcpad_template = gst.PadTemplate(
'src', gst.PAD_SRC, gst.PAD_ALWAYS,
gst.caps_from_string('text/uri-list'))
__gsttemplates__ = (sinkpad_template, srcpad_template)
def convert(self, data):
return parse_asx(data)
class UriListElement(BasePlaylistElement):
__gstdetails__ = ('URIListDemuxer',
'Demuxer',
'Convert a text/uri-list to a stream',
'Mopidy')
sinkpad_template = gst.PadTemplate(
'sink', gst.PAD_SINK, gst.PAD_ALWAYS,
gst.caps_from_string('text/uri-list'))
srcpad_template = gst.PadTemplate(
'src', gst.PAD_SRC, gst.PAD_ALWAYS,
gst.caps_new_any())
ghost_srcpad = True # We need to hook this up to our internal decodebin
__gsttemplates__ = (sinkpad_template, srcpad_template)
def __init__(self):
super(UriListElement, self).__init__()
self.uridecodebin = gst.element_factory_make('uridecodebin')
self.uridecodebin.connect('pad-added', self.pad_added)
# Limit to anycaps so we get a single stream out, letting other
# elements downstream figure out actual muxing
self.uridecodebin.set_property('caps', gst.caps_new_any())
def pad_added(self, src, pad):
self.srcpad.set_target(pad)
pad.add_event_probe(self.pad_event)
def pad_event(self, pad, event):
if event.has_name('urilist-played'):
error = gst.GError(gst.RESOURCE_ERROR, gst.RESOURCE_ERROR_FAILED,
b'Nested playlists not supported.')
message = b'Playlists pointing to other playlists is not supported'
self.post_message(gst.message_new_error(self, error, message))
return 1 # GST_PAD_PROBE_OK
def handle(self, uris):
struct = gst.Structure('urilist-played')
event = gst.event_new_custom(gst.EVENT_CUSTOM_UPSTREAM, struct)
self.sinkpad.push_event(event)
# TODO: hookup about to finish and errors to rest of URIs so we
# round robin, only giving up once all have been tried.
# TODO: uris could be empty.
self.add(self.uridecodebin)
self.uridecodebin.set_state(gst.STATE_READY)
self.uridecodebin.set_property('uri', uris[0])
self.uridecodebin.sync_state_with_parent()
return True # Make sure we consume the EOS that triggered us.
def convert(self, data):
return parse_urilist(data)
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_element(element_class):
gobject.type_register(element_class)
gst.element_register(
element_class, element_class.__name__.lower(), gst.RANK_MARGINAL)
def register_elements():
register_element(M3uDecoder)
register_element(PlsDecoder)
register_element(XspfDecoder)
register_element(AsxDecoder)
register_element(UriListElement)
# 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://'):
register_element(IcySrc)

View File

@ -21,7 +21,9 @@ class Extension(ext.Extension):
schema['media_dir'] = config.Path()
schema['playlists_dir'] = config.Path()
schema['tag_cache_file'] = config.Path()
schema['scan_timeout'] = config.Integer(minimum=0)
schema['scan_timeout'] = config.Integer(
minimum=1000, maximum=1000*60*60)
schema['excluded_file_extensions'] = config.List(optional=True)
return schema
def validate_environment(self):

View File

@ -4,3 +4,9 @@ media_dir = $XDG_MUSIC_DIR
playlists_dir = $XDG_DATA_DIR/mopidy/local/playlists
tag_cache_file = $XDG_DATA_DIR/mopidy/local/tag_cache
scan_timeout = 1000
excluded_file_extensions =
.jpeg
.jpg
.png
.txt
.log

View File

@ -27,9 +27,14 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
self._media_dir, self._tag_cache_file)
tracks = parse_mpd_tag_cache(self._tag_cache_file, self._media_dir)
uris_to_remove = set(self._uri_mapping)
for track in tracks:
self._uri_mapping[track.uri] = track
uris_to_remove.discard(track.uri)
for uri in uris_to_remove:
del self._uri_mapping[uri]
logger.info(
'Loaded %d local tracks from %s using %s',
@ -55,17 +60,29 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
values = [values]
# FIXME this is bound to be slow for large libraries
for value in values:
q = value.strip()
if field == 'track_no':
q = value
else:
q = value.strip()
uri_filter = lambda t: q == t.uri
track_filter = lambda t: q == t.name
album_filter = lambda t: q == getattr(t, 'album', Album()).name
artist_filter = lambda t: filter(
lambda a: q == a.name, t.artists)
albumartist_filter = lambda t: any([
q == a.name
for a in getattr(t.album, 'artists', [])])
track_no_filter = lambda t: q == t.track_no
date_filter = lambda t: q == t.date
any_filter = lambda t: (
track_filter(t) or album_filter(t) or
artist_filter(t) or uri_filter(t))
uri_filter(t) or
track_filter(t) or
album_filter(t) or
artist_filter(t) or
albumartist_filter(t) or
track_no_filter(t) or
date_filter(t))
if field == 'uri':
result_tracks = filter(uri_filter, result_tracks)
@ -75,6 +92,10 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(album_filter, result_tracks)
elif field == 'artist':
result_tracks = filter(artist_filter, result_tracks)
elif field == 'albumartist':
result_tracks = filter(albumartist_filter, result_tracks)
elif field == 'track_no':
result_tracks = filter(track_no_filter, result_tracks)
elif field == 'date':
result_tracks = filter(date_filter, result_tracks)
elif field == 'any':
@ -97,7 +118,10 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
values = [values]
# FIXME this is bound to be slow for large libraries
for value in values:
q = value.strip().lower()
if field == 'track_no':
q = value
else:
q = value.strip().lower()
uri_filter = lambda t: q in t.uri.lower()
track_filter = lambda t: q in t.name.lower()
@ -105,9 +129,19 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
t, 'album', Album()).name.lower()
artist_filter = lambda t: filter(
lambda a: q in a.name.lower(), t.artists)
albumartist_filter = lambda t: any([
q in a.name.lower()
for a in getattr(t.album, 'artists', [])])
track_no_filter = lambda t: q == t.track_no
date_filter = lambda t: t.date and t.date.startswith(q)
any_filter = lambda t: track_filter(t) or album_filter(t) or \
artist_filter(t) or uri_filter(t)
any_filter = lambda t: (
uri_filter(t) or
track_filter(t) or
album_filter(t) or
artist_filter(t) or
albumartist_filter(t) or
track_no_filter(t) or
date_filter(t))
if field == 'uri':
result_tracks = filter(uri_filter, result_tracks)
@ -117,6 +151,10 @@ class LocalLibraryProvider(base.BaseLibraryProvider):
result_tracks = filter(album_filter, result_tracks)
elif field == 'artist':
result_tracks = filter(artist_filter, result_tracks)
elif field == 'albumartist':
result_tracks = filter(albumartist_filter, result_tracks)
elif field == 'track_no':
result_tracks = filter(track_no_filter, result_tracks)
elif field == 'date':
result_tracks = filter(date_filter, result_tracks)
elif field == 'any':

View File

@ -13,7 +13,7 @@ class LocalPlaybackProvider(base.BasePlaybackProvider):
def change_track(self, track):
media_dir = self.backend.config['local']['media_dir']
# TODO: check that type is correct.
file_path = path.uri_to_path(track.uri).split(':', 1)[1]
file_path = path.uri_to_path(track.uri).split(b':', 1)[1]
file_path = os.path.join(media_dir, file_path)
track = track.copy(uri=path.path_to_uri(file_path))
return super(LocalPlaybackProvider, self).change_track(track)

View File

@ -6,7 +6,7 @@ import os
import shutil
from mopidy.backends import base, listener
from mopidy.models import Playlist
from mopidy.models import Playlist, Track
from mopidy.utils import formatting, path
from .translator import parse_m3u
@ -51,12 +51,11 @@ class LocalPlaylistsProvider(base.BasePlaylistsProvider):
tracks = []
for track_uri in parse_m3u(m3u, self._media_dir):
try:
# TODO We must use core.library.lookup() to support tracks
# from other backends
result = self.backend.library.lookup(track_uri)
if result:
tracks += self.backend.library.lookup(track_uri)
except LookupError as ex:
logger.warning('Playlist item could not be added: %s', ex)
else:
tracks.append(Track(uri=track_uri))
playlist = Playlist(uri=uri, name=name, tracks=tracks)
playlists.append(playlist)

View File

@ -1,36 +0,0 @@
from __future__ import unicode_literals
import os
import mopidy
from mopidy import config, exceptions, ext
class Extension(ext.Extension):
dist_name = 'Mopidy-Spotify'
ext_name = 'spotify'
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
def get_config_schema(self):
schema = super(Extension, self).get_config_schema()
schema['username'] = config.String()
schema['password'] = config.Secret()
schema['bitrate'] = config.Integer(choices=(96, 160, 320))
schema['timeout'] = config.Integer(minimum=0)
schema['cache_dir'] = config.Path()
return schema
def validate_environment(self):
try:
import spotify # noqa
except ImportError as e:
raise exceptions.ExtensionError('pyspotify library not found', e)
def get_backend_classes(self):
from .actor import SpotifyBackend
return [SpotifyBackend]

View File

@ -1,37 +0,0 @@
from __future__ import unicode_literals
import logging
import pykka
from mopidy.backends import base
from mopidy.backends.spotify.library import SpotifyLibraryProvider
from mopidy.backends.spotify.playback import SpotifyPlaybackProvider
from mopidy.backends.spotify.session_manager import SpotifySessionManager
from mopidy.backends.spotify.playlists import SpotifyPlaylistsProvider
logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyBackend(pykka.ThreadingActor, base.Backend):
def __init__(self, config, audio):
super(SpotifyBackend, self).__init__()
self.config = config
self.library = SpotifyLibraryProvider(backend=self)
self.playback = SpotifyPlaybackProvider(audio=audio, backend=self)
self.playlists = SpotifyPlaylistsProvider(backend=self)
self.uri_schemes = ['spotify']
self.spotify = SpotifySessionManager(
config, audio=audio, backend_ref=self.actor_ref)
def on_start(self):
logger.info('Mopidy uses SPOTIFY(R) CORE')
logger.debug('Connecting to Spotify')
self.spotify.start()
def on_stop(self):
self.spotify.logout()

View File

@ -1,51 +0,0 @@
from __future__ import unicode_literals
import logging
from spotify.manager import SpotifyContainerManager as \
PyspotifyContainerManager
logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyContainerManager(PyspotifyContainerManager):
def __init__(self, session_manager):
PyspotifyContainerManager.__init__(self)
self.session_manager = session_manager
def container_loaded(self, container, userdata):
"""Callback used by pyspotify"""
logger.debug('Callback called: playlist container loaded')
self.session_manager.refresh_playlists()
count = 0
for playlist in self.session_manager.session.playlist_container():
if playlist.type() == 'playlist':
self.session_manager.playlist_manager.watch(playlist)
count += 1
logger.debug('Watching %d playlist(s) for changes', count)
def playlist_added(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: playlist added at position %d', position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
def playlist_moved(self, container, playlist, old_position, new_position,
userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: playlist "%s" moved from position %d to %d',
playlist.name(), old_position, new_position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.
def playlist_removed(self, container, playlist, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: playlist "%s" removed from position %d',
playlist.name(), position)
# container_loaded() is called after this callback, so we do not need
# to handle this callback.

View File

@ -1,7 +0,0 @@
[spotify]
enabled = true
username =
password =
bitrate = 160
timeout = 10
cache_dir = $XDG_CACHE_DIR/mopidy/spotify

View File

@ -1,211 +0,0 @@
from __future__ import unicode_literals
import logging
import time
import urllib
import pykka
from spotify import Link, SpotifyError
from mopidy.backends import base
from mopidy.models import Track, SearchResult
from . import translator
logger = logging.getLogger('mopidy.backends.spotify')
TRACK_AVAILABLE = 1
class SpotifyTrack(Track):
"""Proxy object for unloaded Spotify tracks."""
def __init__(self, uri=None, track=None):
super(SpotifyTrack, self).__init__()
if (uri and track) or (not uri and not track):
raise AttributeError('uri or track must be provided')
elif uri:
self._spotify_track = Link.from_string(uri).as_track()
elif track:
self._spotify_track = track
self._unloaded_track = Track(uri=uri, name='[loading...]')
self._track = None
@property
def _proxy(self):
if self._track:
return self._track
elif self._spotify_track.is_loaded():
self._track = translator.to_mopidy_track(self._spotify_track)
return self._track
else:
return self._unloaded_track
def __getattribute__(self, name):
if name.startswith('_'):
return super(SpotifyTrack, self).__getattribute__(name)
return self._proxy.__getattribute__(name)
def __repr__(self):
return self._proxy.__repr__()
def __hash__(self):
return hash(self._proxy.uri)
def __eq__(self, other):
if not isinstance(other, Track):
return False
return self._proxy.uri == other.uri
def copy(self, **values):
return self._proxy.copy(**values)
class SpotifyLibraryProvider(base.BaseLibraryProvider):
def __init__(self, *args, **kwargs):
super(SpotifyLibraryProvider, self).__init__(*args, **kwargs)
self._timeout = self.backend.config['spotify']['timeout']
def find_exact(self, query=None, uris=None):
return self.search(query=query, uris=uris)
def lookup(self, uri):
try:
link = Link.from_string(uri)
if link.type() == Link.LINK_TRACK:
return self._lookup_track(uri)
if link.type() == Link.LINK_ALBUM:
return self._lookup_album(uri)
elif link.type() == Link.LINK_ARTIST:
return self._lookup_artist(uri)
elif link.type() == Link.LINK_PLAYLIST:
return self._lookup_playlist(uri)
else:
return []
except SpotifyError as error:
logger.debug(u'Failed to lookup "%s": %s', uri, error)
return []
def _lookup_track(self, uri):
track = Link.from_string(uri).as_track()
self._wait_for_object_to_load(track)
if track.is_loaded():
if track.availability() == TRACK_AVAILABLE:
return [SpotifyTrack(track=track)]
else:
return []
else:
return [SpotifyTrack(uri=uri)]
def _lookup_album(self, uri):
album = Link.from_string(uri).as_album()
album_browser = self.backend.spotify.session.browse_album(album)
self._wait_for_object_to_load(album_browser)
return [
SpotifyTrack(track=t)
for t in album_browser if t.availability() == TRACK_AVAILABLE]
def _lookup_artist(self, uri):
artist = Link.from_string(uri).as_artist()
artist_browser = self.backend.spotify.session.browse_artist(artist)
self._wait_for_object_to_load(artist_browser)
return [
SpotifyTrack(track=t)
for t in artist_browser if t.availability() == TRACK_AVAILABLE]
def _lookup_playlist(self, uri):
playlist = Link.from_string(uri).as_playlist()
self._wait_for_object_to_load(playlist)
return [
SpotifyTrack(track=t)
for t in playlist if t.availability() == TRACK_AVAILABLE]
def _wait_for_object_to_load(self, spotify_obj, timeout=None):
# XXX Sleeping to wait for the Spotify object to load is an ugly hack,
# but it works. We should look into other solutions for this.
if timeout is None:
timeout = self._timeout
wait_until = time.time() + timeout
while not spotify_obj.is_loaded():
time.sleep(0.1)
if time.time() > wait_until:
logger.debug(
'Timeout: Spotify object did not load in %ds', timeout)
return
def refresh(self, uri=None):
pass # TODO
def search(self, query=None, uris=None):
# TODO Only return results within URI roots given by ``uris``
if not query:
return self._get_all_tracks()
uris = query.get('uri', [])
if uris:
tracks = []
for uri in uris:
tracks += self.lookup(uri)
if len(uris) == 1:
uri = uris[0]
else:
uri = 'spotify:search'
return SearchResult(uri=uri, tracks=tracks)
spotify_query = self._translate_search_query(query)
logger.debug('Spotify search query: %s' % spotify_query)
future = pykka.ThreadingFuture()
def callback(results, userdata=None):
search_result = SearchResult(
uri='spotify:search:%s' % (
urllib.quote(results.query().encode('utf-8'))),
albums=[
translator.to_mopidy_album(a) for a in results.albums()],
artists=[
translator.to_mopidy_artist(a) for a in results.artists()],
tracks=[
translator.to_mopidy_track(t) for t in results.tracks()])
future.set(search_result)
if not self.backend.spotify.connected.is_set():
logger.debug('Not connected: Spotify search cancelled')
return SearchResult(uri='spotify:search')
self.backend.spotify.session.search(
spotify_query, callback,
album_count=200, artist_count=200, track_count=200)
try:
return future.get(timeout=self._timeout)
except pykka.Timeout:
logger.debug(
'Timeout: Spotify search did not return in %ds', self._timeout)
return SearchResult(uri='spotify:search')
def _get_all_tracks(self):
# Since we can't search for the entire Spotify library, we return
# all tracks in the playlists when the query is empty.
tracks = []
for playlist in self.backend.playlists.playlists:
tracks += playlist.tracks
return SearchResult(uri='spotify:search', tracks=tracks)
def _translate_search_query(self, mopidy_query):
spotify_query = []
for (field, values) in mopidy_query.iteritems():
if field == 'date':
field = 'year'
if not hasattr(values, '__iter__'):
values = [values]
for value in values:
if field == 'any':
spotify_query.append(value)
elif field == 'year':
value = int(value.split('-')[0]) # Extract year
spotify_query.append('%s:%d' % (field, value))
else:
spotify_query.append('%s:"%s"' % (field, value))
spotify_query = ' '.join(spotify_query)
return spotify_query

View File

@ -1,94 +0,0 @@
from __future__ import unicode_literals
import logging
import functools
from spotify import Link, SpotifyError
from mopidy import audio
from mopidy.backends import base
logger = logging.getLogger('mopidy.backends.spotify')
def need_data_callback(spotify_backend, length_hint):
spotify_backend.playback.on_need_data(length_hint)
def enough_data_callback(spotify_backend):
spotify_backend.playback.on_enough_data()
def seek_data_callback(spotify_backend, time_position):
spotify_backend.playback.on_seek_data(time_position)
class SpotifyPlaybackProvider(base.BasePlaybackProvider):
# These GStreamer caps matches the audio data provided by libspotify
_caps = (
'audio/x-raw-int, endianness=(int)1234, channels=(int)2, '
'width=(int)16, depth=(int)16, signed=(boolean)true, '
'rate=(int)44100')
def __init__(self, *args, **kwargs):
super(SpotifyPlaybackProvider, self).__init__(*args, **kwargs)
self._first_seek = False
def play(self, track):
if track.uri is None:
return False
spotify_backend = self.backend.actor_ref.proxy()
need_data_callback_bound = functools.partial(
need_data_callback, spotify_backend)
enough_data_callback_bound = functools.partial(
enough_data_callback, spotify_backend)
seek_data_callback_bound = functools.partial(
seek_data_callback, spotify_backend)
self._first_seek = True
try:
self.backend.spotify.session.load(
Link.from_string(track.uri).as_track())
self.backend.spotify.session.play(1)
self.backend.spotify.buffer_timestamp = 0
self.audio.prepare_change()
self.audio.set_appsrc(
self._caps,
need_data=need_data_callback_bound,
enough_data=enough_data_callback_bound,
seek_data=seek_data_callback_bound)
self.audio.start_playback()
self.audio.set_metadata(track)
return True
except SpotifyError as e:
logger.info('Playback of %s failed: %s', track.uri, e)
return False
def stop(self):
self.backend.spotify.session.play(0)
return super(SpotifyPlaybackProvider, self).stop()
def on_need_data(self, length_hint):
logger.debug('playback.on_need_data(%d) called', length_hint)
self.backend.spotify.push_audio_data = True
def on_enough_data(self):
logger.debug('playback.on_enough_data() called')
self.backend.spotify.push_audio_data = False
def on_seek_data(self, time_position):
logger.debug('playback.on_seek_data(%d) called', time_position)
if time_position == 0 and self._first_seek:
self._first_seek = False
logger.debug('Skipping seek due to issue #300')
return
self.backend.spotify.buffer_timestamp = audio.millisecond_to_clocktime(
time_position)
self.backend.spotify.session.seek(time_position)

View File

@ -1,105 +0,0 @@
from __future__ import unicode_literals
import datetime
import logging
from spotify.manager import SpotifyPlaylistManager as PyspotifyPlaylistManager
logger = logging.getLogger('mopidy.backends.spotify')
class SpotifyPlaylistManager(PyspotifyPlaylistManager):
def __init__(self, session_manager):
PyspotifyPlaylistManager.__init__(self)
self.session_manager = session_manager
def tracks_added(self, playlist, tracks, position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: '
'%d track(s) added to position %d in playlist "%s"',
len(tracks), position, playlist.name())
self.session_manager.refresh_playlists()
def tracks_moved(self, playlist, tracks, new_position, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: '
'%d track(s) moved to position %d in playlist "%s"',
len(tracks), new_position, playlist.name())
self.session_manager.refresh_playlists()
def tracks_removed(self, playlist, tracks, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: '
'%d track(s) removed from playlist "%s"',
len(tracks), playlist.name())
self.session_manager.refresh_playlists()
def playlist_renamed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Playlist renamed to "%s"', playlist.name())
self.session_manager.refresh_playlists()
def playlist_state_changed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: The state of playlist "%s" changed',
playlist.name())
def playlist_update_in_progress(self, playlist, done, userdata):
"""Callback used by pyspotify"""
if done:
logger.debug(
'Callback called: Update of playlist "%s" done',
playlist.name())
else:
logger.debug(
'Callback called: Update of playlist "%s" in progress',
playlist.name())
def playlist_metadata_updated(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Metadata updated for playlist "%s"',
playlist.name())
def track_created_changed(self, playlist, position, user, when, userdata):
"""Callback used by pyspotify"""
when = datetime.datetime.fromtimestamp(when)
logger.debug(
'Callback called: Created by/when for track %d in playlist '
'"%s" changed to user "N/A" and time "%s"',
position, playlist.name(), when)
def track_message_changed(self, playlist, position, message, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Message for track %d in playlist '
'"%s" changed to "%s"', position, playlist.name(), message)
def track_seen_changed(self, playlist, position, seen, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Seen attribute for track %d in playlist '
'"%s" changed to "%s"', position, playlist.name(), seen)
def description_changed(self, playlist, description, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Description changed for playlist "%s" to "%s"',
playlist.name(), description)
def subscribers_changed(self, playlist, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Subscribers changed for playlist "%s"',
playlist.name())
def image_changed(self, playlist, image, userdata):
"""Callback used by pyspotify"""
logger.debug(
'Callback called: Image changed for playlist "%s"',
playlist.name())

View File

@ -1,22 +0,0 @@
from __future__ import unicode_literals
from mopidy.backends import base
class SpotifyPlaylistsProvider(base.BasePlaylistsProvider):
def create(self, name):
pass # TODO
def delete(self, uri):
pass # TODO
def lookup(self, uri):
for playlist in self._playlists:
if playlist.uri == uri:
return playlist
def refresh(self):
pass # TODO
def save(self, playlist):
pass # TODO

View File

@ -1,201 +0,0 @@
from __future__ import unicode_literals
import logging
import os
import threading
from spotify.manager import SpotifySessionManager as PyspotifySessionManager
from mopidy import audio
from mopidy.backends.listener import BackendListener
from mopidy.utils import process, versioning
from . import translator
from .container_manager import SpotifyContainerManager
from .playlist_manager import SpotifyPlaylistManager
logger = logging.getLogger('mopidy.backends.spotify')
BITRATES = {96: 2, 160: 0, 320: 1}
class SpotifySessionManager(process.BaseThread, PyspotifySessionManager):
cache_location = None
settings_location = None
appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key')
user_agent = 'Mopidy %s' % versioning.get_version()
def __init__(self, config, audio, backend_ref):
self.cache_location = config['spotify']['cache_dir']
self.settings_location = config['spotify']['cache_dir']
full_proxy = ''
if config['proxy']['hostname']:
full_proxy = config['proxy']['hostname']
if config['proxy']['port']:
full_proxy += ':' + str(config['proxy']['port'])
if config['proxy']['scheme']:
full_proxy = config['proxy']['scheme'] + "://" + full_proxy
PyspotifySessionManager.__init__(
self, config['spotify']['username'], config['spotify']['password'],
proxy=full_proxy,
proxy_username=config['proxy']['username'],
proxy_password=config['proxy']['password'])
process.BaseThread.__init__(self)
self.name = 'SpotifyThread'
self.audio = audio
self.backend = None
self.backend_ref = backend_ref
self.bitrate = config['spotify']['bitrate']
self.connected = threading.Event()
self.push_audio_data = True
self.buffer_timestamp = 0
self.container_manager = None
self.playlist_manager = None
self._initial_data_receive_completed = False
def run_inside_try(self):
self.backend = self.backend_ref.proxy()
self.connect()
def logged_in(self, session, error):
"""Callback used by pyspotify"""
if error:
logger.error('Spotify login error: %s', error)
return
logger.info('Connected to Spotify')
# To work with both pyspotify 1.9 and 1.10
if not hasattr(self, 'session'):
self.session = session
logger.debug('Preferred Spotify bitrate is %d kbps', self.bitrate)
session.set_preferred_bitrate(BITRATES[self.bitrate])
self.container_manager = SpotifyContainerManager(self)
self.playlist_manager = SpotifyPlaylistManager(self)
self.container_manager.watch(session.playlist_container())
self.connected.set()
def logged_out(self, session):
"""Callback used by pyspotify"""
logger.info('Disconnected from Spotify')
self.connected.clear()
def metadata_updated(self, session):
"""Callback used by pyspotify"""
logger.debug('Callback called: Metadata updated')
def connection_error(self, session, error):
"""Callback used by pyspotify"""
if error is None:
logger.info('Spotify connection OK')
else:
logger.error('Spotify connection error: %s', error)
if self.audio.state.get() == audio.PlaybackState.PLAYING:
self.backend.playback.pause()
def message_to_user(self, session, message):
"""Callback used by pyspotify"""
logger.debug('User message: %s', message.strip())
def music_delivery(self, session, frames, frame_size, num_frames,
sample_type, sample_rate, channels):
"""Callback used by pyspotify"""
if not self.push_audio_data:
return 0
assert sample_type == 0, 'Expects 16-bit signed integer samples'
capabilites = """
audio/x-raw-int,
endianness=(int)1234,
channels=(int)%(channels)d,
width=(int)16,
depth=(int)16,
signed=(boolean)true,
rate=(int)%(sample_rate)d
""" % {
'sample_rate': sample_rate,
'channels': channels,
}
duration = audio.calculate_duration(num_frames, sample_rate)
buffer_ = audio.create_buffer(bytes(frames),
capabilites=capabilites,
timestamp=self.buffer_timestamp,
duration=duration)
self.buffer_timestamp += duration
if self.audio.emit_data(buffer_).get():
return num_frames
else:
return 0
def play_token_lost(self, session):
"""Callback used by pyspotify"""
logger.debug('Play token lost')
self.backend.playback.pause()
def log_message(self, session, data):
"""Callback used by pyspotify"""
logger.debug('System message: %s' % data.strip())
if 'offline-mgr' in data and 'files unlocked' in data:
# XXX This is a very very fragile and ugly hack, but we get no
# proper event when libspotify is done with initial data loading.
# We delay the expensive refresh of Mopidy's playlists until this
# message arrives. This way, we avoid doing the refresh once for
# every playlist or other change. This reduces the time from
# startup until the Spotify backend is ready from 35s to 12s in one
# test with clean Spotify cache. In cases with an outdated cache
# the time improvements should be a lot greater.
if not self._initial_data_receive_completed:
self._initial_data_receive_completed = True
self.refresh_playlists()
def end_of_track(self, session):
"""Callback used by pyspotify"""
logger.debug('End of data stream reached')
self.audio.emit_end_of_stream()
def refresh_playlists(self):
"""Refresh the playlists in the backend with data from Spotify"""
if not self._initial_data_receive_completed:
logger.debug('Still getting data; skipped refresh of playlists')
return
playlists = []
folders = []
for spotify_playlist in self.session.playlist_container():
if spotify_playlist.type() == 'folder_start':
folders.append(spotify_playlist)
if spotify_playlist.type() == 'folder_end':
folders.pop()
playlists.append(translator.to_mopidy_playlist(
spotify_playlist, folders=folders,
bitrate=self.bitrate, username=self.username))
playlists.append(translator.to_mopidy_playlist(
self.session.starred(),
bitrate=self.bitrate, username=self.username))
playlists = filter(None, playlists)
self.backend.playlists.playlists = playlists
logger.info('Loaded %d Spotify playlists', len(playlists))
BackendListener.send('playlists_loaded')
def logout(self):
"""Log out from spotify"""
logger.debug('Logging out from Spotify')
# To work with both pyspotify 1.9 and 1.10
if getattr(self, 'session', None):
self.session.logout()

View File

@ -1,97 +0,0 @@
from __future__ import unicode_literals
import logging
import spotify
from mopidy.models import Artist, Album, Track, Playlist
logger = logging.getLogger('mopidy.backends.spotify')
artist_cache = {}
album_cache = {}
track_cache = {}
def to_mopidy_artist(spotify_artist):
if spotify_artist is None:
return
uri = str(spotify.Link.from_artist(spotify_artist))
if uri in artist_cache:
return artist_cache[uri]
if not spotify_artist.is_loaded():
return Artist(uri=uri, name='[loading...]')
artist_cache[uri] = Artist(uri=uri, name=spotify_artist.name())
return artist_cache[uri]
def to_mopidy_album(spotify_album):
if spotify_album is None:
return
uri = str(spotify.Link.from_album(spotify_album))
if uri in album_cache:
return album_cache[uri]
if not spotify_album.is_loaded():
return Album(uri=uri, name='[loading...]')
album_cache[uri] = Album(
uri=uri,
name=spotify_album.name(),
artists=[to_mopidy_artist(spotify_album.artist())],
date=spotify_album.year())
return album_cache[uri]
def to_mopidy_track(spotify_track, bitrate=None):
if spotify_track is None:
return
uri = str(spotify.Link.from_track(spotify_track, 0))
if uri in track_cache:
return track_cache[uri]
if not spotify_track.is_loaded():
return Track(uri=uri, name='[loading...]')
spotify_album = spotify_track.album()
if spotify_album is not None and spotify_album.is_loaded():
date = spotify_album.year()
else:
date = None
track_cache[uri] = Track(
uri=uri,
name=spotify_track.name(),
artists=[to_mopidy_artist(a) for a in spotify_track.artists()],
album=to_mopidy_album(spotify_track.album()),
track_no=spotify_track.index(),
date=date,
length=spotify_track.duration(),
bitrate=bitrate)
return track_cache[uri]
def to_mopidy_playlist(
spotify_playlist, folders=None, bitrate=None, username=None):
if spotify_playlist is None or spotify_playlist.type() != 'playlist':
return
try:
uri = str(spotify.Link.from_playlist(spotify_playlist))
except spotify.SpotifyError as e:
logger.debug('Spotify playlist translation error: %s', e)
return
if not spotify_playlist.is_loaded():
return Playlist(uri=uri, name='[loading...]')
name = spotify_playlist.name()
if folders:
folder_names = '/'.join(folder.name() for folder in folders)
name = folder_names + '/' + name
tracks = [
to_mopidy_track(spotify_track, bitrate=bitrate)
for spotify_track in spotify_playlist
if not spotify_track.is_local()
]
if not name:
name = 'Starred'
# Tracks in the Starred playlist are in reverse order from the official
# client.
tracks.reverse()
if spotify_playlist.owner().canonical_name() != username:
name += ' by ' + spotify_playlist.owner().canonical_name()
return Playlist(uri=uri, name=name, tracks=tracks)

View File

@ -2,8 +2,10 @@ from __future__ import unicode_literals
import ConfigParser as configparser
import io
import itertools
import logging
import os.path
import re
from mopidy.config import keyring
from mopidy.config.schemas import * # noqa
@ -145,6 +147,53 @@ def _format(config, comments, schemas, display):
return b'\n'.join(output)
def _preprocess(config_string):
"""Convert a raw config into a form that preserves comments etc."""
results = ['[__COMMENTS__]']
counter = itertools.count(0)
section_re = re.compile(r'^(\[[^\]]+\])\s*(.+)$')
blank_line_re = re.compile(r'^\s*$')
comment_re = re.compile(r'^(#|;)')
inline_comment_re = re.compile(r' ;')
def newlines(match):
return '__BLANK%d__ =' % next(counter)
def comments(match):
if match.group(1) == '#':
return '__HASH%d__ =' % next(counter)
elif match.group(1) == ';':
return '__SEMICOLON%d__ =' % next(counter)
def inlinecomments(match):
return '\n__INLINE%d__ =' % next(counter)
def sections(match):
return '%s\n__SECTION%d__ = %s' % (
match.group(1), next(counter), match.group(2))
for line in config_string.splitlines():
line = blank_line_re.sub(newlines, line)
line = section_re.sub(sections, line)
line = comment_re.sub(comments, line)
line = inline_comment_re.sub(inlinecomments, line)
results.append(line)
return '\n'.join(results)
def _postprocess(config_string):
"""Converts a preprocessed config back to original form."""
flags = re.IGNORECASE | re.MULTILINE
result = re.sub(r'^\[__COMMENTS__\](\n|$)', '', config_string, flags=flags)
result = re.sub(r'\n__INLINE\d+__ =(.*)$', ' ;\g<1>', result, flags=flags)
result = re.sub(r'^__HASH\d+__ =(.*)$', '#\g<1>', result, flags=flags)
result = re.sub(r'^__SEMICOLON\d+__ =(.*)$', ';\g<1>', result, flags=flags)
result = re.sub(r'\n__SECTION\d+__ =(.*)$', '\g<1>', result, flags=flags)
result = re.sub(r'^__BLANK\d+__ =$', '', result, flags=flags)
return result
class Proxy(collections.Mapping):
def __init__(self, data):
self._data = data

View File

@ -132,11 +132,25 @@ class CoreListener(object):
"""
pass
def volume_changed(self):
def volume_changed(self, volume):
"""
Called whenever the volume is changed.
*MAY* be implemented by actor.
:param volume: the new volume in the range [0..100]
:type volume: int
"""
pass
def mute_changed(self, mute):
"""
Called whenever the mute state is changed.
*MAY* be implemented by actor.
:param mute: the new mute state
:type mute: boolean
"""
pass

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals
import logging
import random
import urlparse
from mopidy.audio import PlaybackState
@ -21,9 +20,8 @@ class PlaybackController(object):
self.core = core
self._state = PlaybackState.STOPPED
self._shuffled = []
self._first_shuffle = True
self._volume = None
self._mute = False
def _get_backend(self):
if self.current_tl_track is None:
@ -34,22 +32,6 @@ class PlaybackController(object):
### Properties
def get_consume(self):
return getattr(self, '_consume', False)
def set_consume(self, value):
if self.get_consume() != value:
self._trigger_options_changed()
return setattr(self, '_consume', value)
consume = property(get_consume, set_consume)
"""
:class:`True`
Tracks are removed from the playlist when they have been played.
:class:`False`
Tracks are not removed from the playlist.
"""
def get_current_tl_track(self):
return self.current_tl_track
@ -69,56 +51,6 @@ class PlaybackController(object):
Read-only. Extracted from :attr:`current_tl_track` for convenience.
"""
def get_random(self):
return getattr(self, '_random', False)
def set_random(self, value):
if self.get_random() != value:
self._trigger_options_changed()
return setattr(self, '_random', value)
random = property(get_random, set_random)
"""
:class:`True`
Tracks are selected at random from the playlist.
:class:`False`
Tracks are played in the order of the playlist.
"""
def get_repeat(self):
return getattr(self, '_repeat', False)
def set_repeat(self, value):
if self.get_repeat() != value:
self._trigger_options_changed()
return setattr(self, '_repeat', value)
repeat = property(get_repeat, set_repeat)
"""
:class:`True`
The current playlist is played repeatedly. To repeat a single track,
select both :attr:`repeat` and :attr:`single`.
:class:`False`
The current playlist is played once.
"""
def get_single(self):
return getattr(self, '_single', False)
def set_single(self, value):
if self.get_single() != value:
self._trigger_options_changed()
return setattr(self, '_single', value)
single = property(get_single, set_single)
"""
:class:`True`
Playback is stopped after current song, unless in :attr:`repeat`
mode.
:class:`False`
Playback continues after current song.
"""
def get_state(self):
return self._state
@ -156,119 +88,6 @@ class PlaybackController(object):
time_position = property(get_time_position)
"""Time position in milliseconds."""
def get_tracklist_position(self):
if self.current_tl_track is None:
return None
try:
return self.core.tracklist.tl_tracks.index(self.current_tl_track)
except ValueError:
return None
tracklist_position = property(get_tracklist_position)
"""
The position of the current track in the tracklist.
Read-only.
"""
def get_tl_track_at_eot(self):
tl_tracks = self.core.tracklist.tl_tracks
if not tl_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = tl_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_tl_track is None:
return tl_tracks[0]
if self.repeat and self.single:
return tl_tracks[self.tracklist_position]
if self.repeat and not self.single:
return tl_tracks[(self.tracklist_position + 1) % len(tl_tracks)]
try:
return tl_tracks[self.tracklist_position + 1]
except IndexError:
return None
tl_track_at_eot = property(get_tl_track_at_eot)
"""
The track that will be played at the end of the current track.
Read-only. A :class:`mopidy.models.TlTrack`.
Not necessarily the same track as :attr:`tl_track_at_next`.
"""
def get_tl_track_at_next(self):
tl_tracks = self.core.tracklist.tl_tracks
if not tl_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or self._first_shuffle:
logger.debug('Shuffling tracks')
self._shuffled = tl_tracks
random.shuffle(self._shuffled)
self._first_shuffle = False
if self.random and self._shuffled:
return self._shuffled[0]
if self.current_tl_track is None:
return tl_tracks[0]
if self.repeat:
return tl_tracks[(self.tracklist_position + 1) % len(tl_tracks)]
try:
return tl_tracks[self.tracklist_position + 1]
except IndexError:
return None
tl_track_at_next = property(get_tl_track_at_next)
"""
The track that will be played if calling :meth:`next()`.
Read-only. A :class:`mopidy.models.TlTrack`.
For normal playback this is the next track in the playlist. If repeat
is enabled the next track can loop around the playlist. When random is
enabled this should be a random track, all tracks should be played once
before the list repeats.
"""
def get_tl_track_at_previous(self):
if self.repeat or self.consume or self.random:
return self.current_tl_track
if self.tracklist_position in (None, 0):
return None
return self.core.tracklist.tl_tracks[self.tracklist_position - 1]
tl_track_at_previous = property(get_tl_track_at_previous)
"""
The track that will be played if calling :meth:`previous()`.
A :class:`mopidy.models.TlTrack`.
For normal playback this is the previous track in the playlist. If
random and/or consume is enabled it should return the current track
instead.
"""
def get_volume(self):
if self.audio:
return self.audio.get_volume().get()
@ -288,6 +107,26 @@ class PlaybackController(object):
volume = property(get_volume, set_volume)
"""Volume as int in range [0..100] or :class:`None`"""
def get_mute(self):
if self.audio:
return self.audio.get_mute().get()
else:
# For testing
return self._mute
def set_mute(self, value):
value = bool(value)
if self.audio:
self.audio.set_mute(value)
else:
# For testing
self._mute = value
self._trigger_mute_changed(value)
mute = property(get_mute, set_mute)
"""Mute state as a :class:`True` if muted, :class:`False` otherwise"""
### Methods
def change_track(self, tl_track, on_error_step=1):
@ -318,15 +157,15 @@ class PlaybackController(object):
return
original_tl_track = self.current_tl_track
next_tl_track = self.core.tracklist.eot_track(original_tl_track)
if self.tl_track_at_eot:
if next_tl_track:
self._trigger_track_playback_ended()
self.play(self.tl_track_at_eot)
self.play(next_tl_track)
else:
self.stop(clear_current_track=True)
if self.consume:
self.core.tracklist.remove(tlid=original_tl_track.tlid)
self.core.tracklist.mark_played(original_tl_track)
def on_tracklist_change(self):
"""
@ -334,12 +173,7 @@ class PlaybackController(object):
Used by :class:`mopidy.core.TracklistController`.
"""
self._first_shuffle = True
self._shuffled = []
if (not self.core.tracklist.tl_tracks or
self.current_tl_track not in
self.core.tracklist.tl_tracks):
if self.current_tl_track not in self.core.tracklist.tl_tracks:
self.stop(clear_current_track=True)
def next(self):
@ -349,9 +183,10 @@ class PlaybackController(object):
The current playback state will be kept. If it was playing, playing
will continue. If it was paused, it will still be paused, etc.
"""
if self.tl_track_at_next:
tl_track = self.core.tracklist.next_track(self.current_tl_track)
if tl_track:
self._trigger_track_playback_ended()
self.change_track(self.tl_track_at_next)
self.change_track(tl_track)
else:
self.stop(clear_current_track=True)
@ -374,37 +209,40 @@ class PlaybackController(object):
:type on_error_step: int, -1 or 1
"""
if tl_track is not None:
assert tl_track in self.core.tracklist.tl_tracks
elif tl_track is None:
assert on_error_step in (-1, 1)
if tl_track is None:
if self.state == PlaybackState.PAUSED:
return self.resume()
elif self.current_tl_track is not None:
tl_track = self.current_tl_track
elif self.current_tl_track is None and on_error_step == 1:
tl_track = self.tl_track_at_next
elif self.current_tl_track is None and on_error_step == -1:
tl_track = self.tl_track_at_previous
if tl_track is not None:
self.current_tl_track = tl_track
self.state = PlaybackState.PLAYING
backend = self._get_backend()
if not backend or not backend.playback.play(tl_track.track).get():
logger.warning('Track is not playable: %s', tl_track.track.uri)
if self.random and self._shuffled:
self._shuffled.remove(tl_track)
if self.current_tl_track is not None:
tl_track = self.current_tl_track
else:
if on_error_step == 1:
# TODO: can cause an endless loop for single track repeat.
self.next()
tl_track = self.core.tracklist.next_track(tl_track)
elif on_error_step == -1:
self.previous()
tl_track = self.core.tracklist.previous_track(tl_track)
if tl_track is None:
return
if self.random and self.current_tl_track in self._shuffled:
self._shuffled.remove(self.current_tl_track)
assert tl_track in self.core.tracklist.tl_tracks
self._trigger_track_playback_started()
self.current_tl_track = tl_track
self.state = PlaybackState.PLAYING
backend = self._get_backend()
success = backend and backend.playback.play(tl_track.track).get()
if success:
self.core.tracklist.mark_playing(tl_track)
self._trigger_track_playback_started()
else:
self.core.tracklist.mark_unplayable(tl_track)
if on_error_step == 1:
# TODO: can cause an endless loop for single track repeat.
self.next()
elif on_error_step == -1:
self.previous()
def previous(self):
"""
@ -414,7 +252,9 @@ class PlaybackController(object):
will continue. If it was paused, it will still be paused, etc.
"""
self._trigger_track_playback_ended()
self.change_track(self.tl_track_at_previous, on_error_step=-1)
tl_track = self.current_tl_track
self.change_track(
self.core.tracklist.previous_track(tl_track), on_error_step=-1)
def resume(self):
"""If paused, resume playing the current track."""
@ -510,14 +350,14 @@ class PlaybackController(object):
'playback_state_changed',
old_state=old_state, new_state=new_state)
def _trigger_options_changed(self):
logger.debug('Triggering options changed event')
listener.CoreListener.send('options_changed')
def _trigger_volume_changed(self, volume):
logger.debug('Triggering volume changed event')
listener.CoreListener.send('volume_changed', volume=volume)
def _trigger_mute_changed(self, mute):
logger.debug('Triggering mute changed event')
listener.CoreListener.send('mute_changed', mute=mute)
def _trigger_seeked(self, time_position):
logger.debug('Triggering seeked event')
listener.CoreListener.send('seeked', time_position=time_position)

View File

@ -15,11 +15,15 @@ class TracklistController(object):
pykka_traversable = True
def __init__(self, core):
self._core = core
self.core = core
self._next_tlid = 0
self._tl_tracks = []
self._version = 0
self._shuffled = []
### Properties
def get_tl_tracks(self):
return self._tl_tracks[:]
@ -51,7 +55,7 @@ class TracklistController(object):
def _increase_version(self):
self._version += 1
self._core.playback.on_tracklist_change()
self.core.playback.on_tracklist_change()
self._trigger_tracklist_changed()
version = property(get_version)
@ -62,6 +66,175 @@ class TracklistController(object):
Is not reset before Mopidy is restarted.
"""
def get_consume(self):
return getattr(self, '_consume', False)
def set_consume(self, value):
if self.get_consume() != value:
self._trigger_options_changed()
return setattr(self, '_consume', value)
consume = property(get_consume, set_consume)
"""
:class:`True`
Tracks are removed from the playlist when they have been played.
:class:`False`
Tracks are not removed from the playlist.
"""
def get_random(self):
return getattr(self, '_random', False)
def set_random(self, value):
if self.get_random() != value:
self._trigger_options_changed()
if value:
self._shuffled = self.tl_tracks
random.shuffle(self._shuffled)
return setattr(self, '_random', value)
random = property(get_random, set_random)
"""
:class:`True`
Tracks are selected at random from the playlist.
:class:`False`
Tracks are played in the order of the playlist.
"""
def get_repeat(self):
return getattr(self, '_repeat', False)
def set_repeat(self, value):
if self.get_repeat() != value:
self._trigger_options_changed()
return setattr(self, '_repeat', value)
repeat = property(get_repeat, set_repeat)
"""
:class:`True`
The current playlist is played repeatedly. To repeat a single track,
select both :attr:`repeat` and :attr:`single`.
:class:`False`
The current playlist is played once.
"""
def get_single(self):
return getattr(self, '_single', False)
def set_single(self, value):
if self.get_single() != value:
self._trigger_options_changed()
return setattr(self, '_single', value)
single = property(get_single, set_single)
"""
:class:`True`
Playback is stopped after current song, unless in :attr:`repeat`
mode.
:class:`False`
Playback continues after current song.
"""
### Methods
def index(self, tl_track):
"""
The position of the given track in the tracklist.
:param tl_track: the track to find the index of
:type tl_track: :class:`mopidy.models.TlTrack`
:rtype: :class:`int` or :class:`None`
"""
try:
return self._tl_tracks.index(tl_track)
except ValueError:
return None
def eot_track(self, tl_track):
"""
The track that will be played after the given track.
Not necessarily the same track as :meth:`next_track`.
:param tl_track: the reference track
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`mopidy.models.TlTrack` or :class:`None`
"""
if self.single and self.repeat:
return tl_track
elif self.single:
return None
# Current difference between next and EOT handling is that EOT needs to
# handle "single", with that out of the way the rest of the logic is
# shared.
return self.next_track(tl_track)
def next_track(self, tl_track):
"""
The track that will be played if calling
:meth:`mopidy.core.PlaybackController.next()`.
For normal playback this is the next track in the playlist. If repeat
is enabled the next track can loop around the playlist. When random is
enabled this should be a random track, all tracks should be played once
before the list repeats.
:param tl_track: the reference track
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`mopidy.models.TlTrack` or :class:`None`
"""
if not self.tl_tracks:
return None
if self.random and not self._shuffled:
if self.repeat or not tl_track:
logger.debug('Shuffling tracks')
self._shuffled = self.tl_tracks
random.shuffle(self._shuffled)
if self.random:
try:
return self._shuffled[0]
except IndexError:
return None
if tl_track is None:
return self.tl_tracks[0]
next_index = self.index(tl_track) + 1
if self.repeat:
next_index %= len(self.tl_tracks)
try:
return self.tl_tracks[next_index]
except IndexError:
return None
def previous_track(self, tl_track):
"""
Returns the track that will be played if calling
:meth:`mopidy.core.PlaybackController.previous()`.
For normal playback this is the previous track in the playlist. If
random and/or consume is enabled it should return the current track
instead.
:param tl_track: the reference track
:type tl_track: :class:`mopidy.models.TlTrack` or :class:`None`
:rtype: :class:`mopidy.models.TlTrack` or :class:`None`
"""
if self.repeat or self.consume or self.random:
return tl_track
position = self.index(tl_track)
if position in (None, 0):
return None
return self.tl_tracks[position - 1]
def add(self, tracks=None, at_position=None, uri=None):
"""
Add the track or list of tracks to the tracklist.
@ -87,7 +260,7 @@ class TracklistController(object):
'tracks or uri must be provided'
if tracks is None and uri is not None:
tracks = self._core.library.lookup(uri)
tracks = self.core.library.lookup(uri)
tl_tracks = []
@ -151,18 +324,6 @@ class TracklistController(object):
lambda ct: getattr(ct.track, key) == value, matches)
return matches
def index(self, tl_track):
"""
Get index of the given :class:`mopidy.models.TlTrack` in the tracklist.
Raises :exc:`ValueError` if not found.
:param tl_track: track to find the index of
:type tl_track: :class:`mopidy.models.TlTrack`
:rtype: int
"""
return self._tl_tracks.index(tl_track)
def move(self, start, end, to_position):
"""
Move the tracks in the slice ``[start:end]`` to ``to_position``.
@ -259,6 +420,34 @@ class TracklistController(object):
"""
return self._tl_tracks[start:end]
def mark_playing(self, tl_track):
"""Private method used by :class:`mopidy.core.PlaybackController`."""
if self.random and tl_track in self._shuffled:
self._shuffled.remove(tl_track)
def mark_unplayable(self, tl_track):
"""Private method used by :class:`mopidy.core.PlaybackController`."""
logger.warning('Track is not playable: %s', tl_track.track.uri)
if self.random and tl_track in self._shuffled:
self._shuffled.remove(tl_track)
def mark_played(self, tl_track):
"""Private method used by :class:`mopidy.core.PlaybackController`."""
if not self.consume:
return False
self.remove(tlid=tl_track.tlid)
return True
def _trigger_tracklist_changed(self):
if self.random:
self._shuffled = self.tl_tracks
random.shuffle(self._shuffled)
else:
self._shuffled = []
logger.debug('Triggering event: tracklist_changed()')
listener.CoreListener.send('tracklist_changed')
def _trigger_options_changed(self):
logger.debug('Triggering options changed event')
listener.CoreListener.send('options_changed')

View File

@ -18,3 +18,7 @@ class MopidyException(Exception):
class ExtensionError(MopidyException):
pass
class ScannerError(MopidyException):
pass

View File

@ -83,8 +83,7 @@ class Extension(object):
"""List of library updater classes
:returns: list of
:class:`~mopidy.backends.base.BaseLibraryUpdateProvider`
subclasses
:class:`~mopidy.backends.base.BaseLibraryUpdateProvider` subclasses
"""
return []

View File

@ -55,3 +55,6 @@ class MpdFrontend(pykka.ThreadingActor, CoreListener):
def volume_changed(self, volume):
self.send_idle('mixer')
def mute_changed(self, mute):
self.send_idle('output')

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from mopidy.frontends.mpd.exceptions import MpdNoExistError
from mopidy.frontends.mpd.protocol import handle_request
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
@handle_request(r'^disableoutput "(?P<outputid>\d+)"$')
@ -13,7 +13,10 @@ def disableoutput(context, outputid):
Turns an output off.
"""
raise MpdNotImplemented # TODO
if int(outputid) == 0:
context.core.playback.set_mute(False)
else:
raise MpdNoExistError('No such audio output', command='disableoutput')
@handle_request(r'^enableoutput "(?P<outputid>\d+)"$')
@ -25,7 +28,10 @@ def enableoutput(context, outputid):
Turns an output on.
"""
raise MpdNotImplemented # TODO
if int(outputid) == 0:
context.core.playback.set_mute(True)
else:
raise MpdNoExistError('No such audio output', command='enableoutput')
@handle_request(r'^outputs$')
@ -37,8 +43,9 @@ def outputs(context):
Shows information about all outputs.
"""
muted = 1 if context.core.playback.get_mute().get() else 0
return [
('outputid', 0),
('outputname', 'Default'),
('outputenabled', 1),
('outputname', 'Mute'),
('outputenabled', muted),
]

View File

@ -5,13 +5,13 @@ import itertools
from mopidy.models import Track
from mopidy.frontends.mpd import translator
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
QUERY_RE = (
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]*"\s?)+)$')
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Aa]lbumartist|[Dd]ate|[Ff]ile|'
r'[Ff]ilename|[Tt]itle|[Tt]rack|[Aa]ny)"? "[^"]*"\s?)+)$')
def _get_field(field, search_results):
@ -54,7 +54,16 @@ def count(context, mpd_query):
- does not add quotes around the tag argument.
- use multiple tag-needle pairs to make more specific searches.
"""
return [('songs', 0), ('playtime', 0)] # TODO
try:
query = translator.query_from_mpd_search_format(mpd_query)
except ValueError:
raise MpdArgError('incorrect arguments', command='count')
results = context.core.library.find_exact(**query).get()
result_tracks = _get_tracks(results)
return [
('songs', len(result_tracks)),
('playtime', sum(track.length for track in result_tracks) / 1000),
]
@handle_request(r'^find ' + QUERY_RE)
@ -91,7 +100,7 @@ def find(context, mpd_query):
return
results = context.core.library.find_exact(**query).get()
result_tracks = []
if 'artist' not in query:
if 'artist' not in query and 'albumartist' not in query:
result_tracks += [_artist_as_track(a) for a in _get_artists(results)]
if 'album' not in query:
result_tracks += [_album_as_track(a) for a in _get_albums(results)]

View File

@ -19,9 +19,9 @@ def consume(context, state):
playlist.
"""
if int(state):
context.core.playback.consume = True
context.core.tracklist.consume = True
else:
context.core.playback.consume = False
context.core.tracklist.consume = False
@handle_request(r'^crossfade "(?P<seconds>\d+)"$')
@ -263,9 +263,9 @@ def random(context, state):
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
context.core.playback.random = True
context.core.tracklist.random = True
else:
context.core.playback.random = False
context.core.tracklist.random = False
@handle_request(r'^repeat (?P<state>[01])$')
@ -279,9 +279,9 @@ def repeat(context, state):
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
"""
if int(state):
context.core.playback.repeat = True
context.core.tracklist.repeat = True
else:
context.core.playback.repeat = False
context.core.tracklist.repeat = False
@handle_request(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
@ -329,7 +329,8 @@ def seek(context, songpos, seconds):
- issues ``seek 1 120`` without quotes around the arguments.
"""
if context.core.playback.tracklist_position.get() != int(songpos):
tl_track = context.core.playback.current_tl_track.get()
if context.core.tracklist.index(tl_track).get() != int(songpos):
playpos(context, songpos)
context.core.playback.seek(int(seconds) * 1000).get()
@ -404,9 +405,9 @@ def single(context, state):
song is repeated if the ``repeat`` mode is enabled.
"""
if int(state):
context.core.playback.single = True
context.core.tracklist.single = True
else:
context.core.playback.single = False
context.core.tracklist.single = False
@handle_request(r'^stop$')

View File

@ -36,10 +36,10 @@ def currentsong(context):
Displays the song info of the current song (same song that is
identified in status).
"""
current_tl_track = context.core.playback.current_tl_track.get()
if current_tl_track is not None:
position = context.core.playback.tracklist_position.get()
return track_to_mpd_format(current_tl_track, position=position)
tl_track = context.core.playback.current_tl_track.get()
if tl_track is not None:
position = context.core.tracklist.index(tl_track).get()
return track_to_mpd_format(tl_track, position=position)
@handle_request(r'^idle$')
@ -178,14 +178,15 @@ def status(context):
'tracklist.length': context.core.tracklist.length,
'tracklist.version': context.core.tracklist.version,
'playback.volume': context.core.playback.volume,
'playback.consume': context.core.playback.consume,
'playback.random': context.core.playback.random,
'playback.repeat': context.core.playback.repeat,
'playback.single': context.core.playback.single,
'tracklist.consume': context.core.tracklist.consume,
'tracklist.random': context.core.tracklist.random,
'tracklist.repeat': context.core.tracklist.repeat,
'tracklist.single': context.core.tracklist.single,
'playback.state': context.core.playback.state,
'playback.current_tl_track': context.core.playback.current_tl_track,
'playback.tracklist_position': (
context.core.playback.tracklist_position),
'tracklist.index': (
context.core.tracklist.index(
context.core.playback.current_tl_track.get())),
'playback.time_position': context.core.playback.time_position,
}
pykka.get_all(futures.values())
@ -218,7 +219,7 @@ def _status_bitrate(futures):
def _status_consume(futures):
if futures['playback.consume'].get():
if futures['tracklist.consume'].get():
return 1
else:
return 0
@ -233,15 +234,15 @@ def _status_playlist_version(futures):
def _status_random(futures):
return int(futures['playback.random'].get())
return int(futures['tracklist.random'].get())
def _status_repeat(futures):
return int(futures['playback.repeat'].get())
return int(futures['tracklist.repeat'].get())
def _status_single(futures):
return int(futures['playback.single'].get())
return int(futures['tracklist.single'].get())
def _status_songid(futures):
@ -253,7 +254,7 @@ def _status_songid(futures):
def _status_songpos(futures):
return futures['playback.tracklist_position'].get()
return futures['tracklist.index'].get()
def _status_state(futures):

View File

@ -166,7 +166,7 @@ def query_from_mpd_list_format(field, mpd_query):
key = tokens[0].lower()
value = tokens[1]
tokens = tokens[2:]
if key not in ('artist', 'album', 'date', 'genre'):
if key not in ('artist', 'album', 'albumartist', 'date', 'genre'):
raise MpdArgError('not able to parse args', command='list')
if not value:
raise ValueError
@ -179,6 +179,48 @@ def query_from_mpd_list_format(field, mpd_query):
raise MpdArgError('not able to parse args', command='list')
# XXX The regexps below should be refactored to reuse common patterns here
# and in mopidy.frontends.mpd.protocol.music_db.QUERY_RE.
MPD_SEARCH_QUERY_RE = re.compile(r"""
\b # Only begin matching at word bundaries
"? # Optional quote around the field type
(?: # A non-capturing group for the field type
[Aa]lbum
| [Aa]rtist
| [Aa]lbumartist
| [Dd]ate
| [Ff]ile
| [Ff]ilename
| [Tt]itle
| [Tt]rack
| [Aa]ny
)
"? # End of optional quote around the field type
\s # A single space
"[^"]+" # Matching a quoted search string
""", re.VERBOSE)
MPD_SEARCH_QUERY_PART_RE = re.compile(r"""
\b # Only begin matching at word bundaries
"? # Optional quote around the field type
(?P<field>( # A capturing group for the field type
[Aa]lbum
| [Aa]rtist
| [Aa]lbumartist
| [Dd]ate
| [Ff]ile
| [Ff]ilename
| [Tt]itle
| [Tt]rack
| [Aa]ny
))
"? # End of optional quote around the field type
\s # A single space
"(?P<what>[^"]+)" # Capturing a quoted search string
""", re.VERBOSE)
def query_from_mpd_search_format(mpd_query):
"""
Parses an MPD ``search`` or ``find`` query and converts it to the Mopidy
@ -187,24 +229,17 @@ def query_from_mpd_search_format(mpd_query):
:param mpd_query: the MPD search query
:type mpd_query: string
"""
# XXX The regexps below should be refactored to reuse common patterns here
# and in mopidy.frontends.mpd.protocol.music_db.
query_pattern = (
r'"?(?:[Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|'
r'[Tt]itle|[Aa]ny)"? "[^"]+"')
query_parts = re.findall(query_pattern, mpd_query)
query_part_pattern = (
r'"?(?P<field>([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ile|[Ff]ilename|'
r'[Tt]itle|[Aa]ny))"? "(?P<what>[^"]+)"')
query_parts = MPD_SEARCH_QUERY_RE.findall(mpd_query)
query = {}
for query_part in query_parts:
m = re.match(query_part_pattern, query_part)
m = MPD_SEARCH_QUERY_PART_RE.match(query_part)
field = m.groupdict()['field'].lower()
if field == 'title':
field = 'track'
elif field == 'track':
field = 'track_no'
elif field in ('file', 'filename'):
field = 'uri'
field = str(field) # Needed for kwargs keys on OS X and Windows
what = m.groupdict()['what']
if not what:
raise ValueError

View File

@ -1,36 +0,0 @@
from __future__ import unicode_literals
import os
import mopidy
from mopidy import config, exceptions, ext
class Extension(ext.Extension):
dist_name = 'Mopidy-MPRIS'
ext_name = 'mpris'
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
def get_config_schema(self):
schema = super(Extension, self).get_config_schema()
schema['desktop_file'] = config.Path()
return schema
def validate_environment(self):
if 'DISPLAY' not in os.environ:
raise exceptions.ExtensionError(
'An X11 $DISPLAY is needed to use D-Bus')
try:
import dbus # noqa
except ImportError as e:
raise exceptions.ExtensionError('dbus library not found', e)
def get_frontend_classes(self):
from .actor import MprisFrontend
return [MprisFrontend]

View File

@ -1,110 +0,0 @@
from __future__ import unicode_literals
import logging
import os
import pykka
from mopidy.core import CoreListener
from mopidy.frontends.mpris import objects
logger = logging.getLogger('mopidy.frontends.mpris')
try:
indicate = None
if 'DISPLAY' in os.environ:
import indicate
except ImportError:
pass
if indicate is None:
logger.debug('Startup notification will not be sent')
class MprisFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(MprisFrontend, self).__init__()
self.config = config
self.core = core
self.indicate_server = None
self.mpris_object = None
def on_start(self):
try:
self.mpris_object = objects.MprisObject(self.config, self.core)
self._send_startup_notification()
except Exception as e:
logger.warning('MPRIS frontend setup failed (%s)', e)
self.stop()
def on_stop(self):
logger.debug('Removing MPRIS object from D-Bus connection...')
if self.mpris_object:
self.mpris_object.remove_from_connection()
self.mpris_object = None
logger.debug('Removed MPRIS object from D-Bus connection')
def _send_startup_notification(self):
"""
Send startup notification using libindicate to make Mopidy appear in
e.g. `Ubunt's sound menu <https://wiki.ubuntu.com/SoundMenu>`_.
A reference to the libindicate server is kept for as long as Mopidy is
running. When Mopidy exits, the server will be unreferenced and Mopidy
will automatically be unregistered from e.g. the sound menu.
"""
if not indicate:
return
logger.debug('Sending startup notification...')
self.indicate_server = indicate.Server()
self.indicate_server.set_type('music.mopidy')
self.indicate_server.set_desktop_file(
self.config['mpris']['desktop_file'])
self.indicate_server.show()
logger.debug('Startup notification sent')
def _emit_properties_changed(self, interface, changed_properties):
if self.mpris_object is None:
return
props_with_new_values = [
(p, self.mpris_object.Get(interface, p))
for p in changed_properties]
self.mpris_object.PropertiesChanged(
interface, dict(props_with_new_values), [])
def track_playback_paused(self, tl_track, time_position):
logger.debug('Received track_playback_paused event')
self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus'])
def track_playback_resumed(self, tl_track, time_position):
logger.debug('Received track_playback_resumed event')
self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus'])
def track_playback_started(self, tl_track):
logger.debug('Received track_playback_started event')
self._emit_properties_changed(
objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata'])
def track_playback_ended(self, tl_track, time_position):
logger.debug('Received track_playback_ended event')
self._emit_properties_changed(
objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata'])
def volume_changed(self, volume):
logger.debug('Received volume_changed event')
self._emit_properties_changed(objects.PLAYER_IFACE, ['Volume'])
def seeked(self, time_position_in_ms):
logger.debug('Received seeked event')
self.mpris_object.Seeked(time_position_in_ms * 1000)
def playlists_loaded(self):
logger.debug('Received playlists_loaded event')
self._emit_properties_changed(
objects.PLAYLISTS_IFACE, ['PlaylistCount'])
def playlist_changed(self, playlist):
logger.debug('Received playlist_changed event')
playlist_id = self.mpris_object.get_playlist_id(playlist.uri)
playlist = (playlist_id, playlist.name, '')
self.mpris_object.PlaylistChanged(playlist)

View File

@ -1,3 +0,0 @@
[mpris]
enabled = true
desktop_file = /usr/share/applications/mopidy.desktop

View File

@ -1,498 +0,0 @@
from __future__ import unicode_literals
import base64
import logging
import os
import dbus
import dbus.mainloop.glib
import dbus.service
import gobject
from mopidy.core import PlaybackState
from mopidy.utils.process import exit_process
logger = logging.getLogger('mopidy.frontends.mpris')
# Must be done before dbus.SessionBus() is called
gobject.threads_init()
dbus.mainloop.glib.threads_init()
BUS_NAME = 'org.mpris.MediaPlayer2.mopidy'
OBJECT_PATH = '/org/mpris/MediaPlayer2'
ROOT_IFACE = 'org.mpris.MediaPlayer2'
PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player'
PLAYLISTS_IFACE = 'org.mpris.MediaPlayer2.Playlists'
class MprisObject(dbus.service.Object):
"""Implements http://www.mpris.org/2.2/spec/"""
properties = None
def __init__(self, config, core):
self.config = config
self.core = core
self.properties = {
ROOT_IFACE: self._get_root_iface_properties(),
PLAYER_IFACE: self._get_player_iface_properties(),
PLAYLISTS_IFACE: self._get_playlists_iface_properties(),
}
bus_name = self._connect_to_dbus()
dbus.service.Object.__init__(self, bus_name, OBJECT_PATH)
def _get_root_iface_properties(self):
return {
'CanQuit': (True, None),
'Fullscreen': (False, None),
'CanSetFullscreen': (False, None),
'CanRaise': (False, None),
# NOTE Change if adding optional track list support
'HasTrackList': (False, None),
'Identity': ('Mopidy', None),
'DesktopEntry': (self.get_DesktopEntry, None),
'SupportedUriSchemes': (self.get_SupportedUriSchemes, None),
# NOTE Return MIME types supported by local backend if support for
# reporting supported MIME types is added
'SupportedMimeTypes': (dbus.Array([], signature='s'), None),
}
def _get_player_iface_properties(self):
return {
'PlaybackStatus': (self.get_PlaybackStatus, None),
'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus),
'Rate': (1.0, self.set_Rate),
'Shuffle': (self.get_Shuffle, self.set_Shuffle),
'Metadata': (self.get_Metadata, None),
'Volume': (self.get_Volume, self.set_Volume),
'Position': (self.get_Position, None),
'MinimumRate': (1.0, None),
'MaximumRate': (1.0, None),
'CanGoNext': (self.get_CanGoNext, None),
'CanGoPrevious': (self.get_CanGoPrevious, None),
'CanPlay': (self.get_CanPlay, None),
'CanPause': (self.get_CanPause, None),
'CanSeek': (self.get_CanSeek, None),
'CanControl': (self.get_CanControl, None),
}
def _get_playlists_iface_properties(self):
return {
'PlaylistCount': (self.get_PlaylistCount, None),
'Orderings': (self.get_Orderings, None),
'ActivePlaylist': (self.get_ActivePlaylist, None),
}
def _connect_to_dbus(self):
logger.debug('Connecting to D-Bus...')
mainloop = dbus.mainloop.glib.DBusGMainLoop()
bus_name = dbus.service.BusName(
BUS_NAME, dbus.SessionBus(mainloop=mainloop))
logger.info('MPRIS server connected to D-Bus')
return bus_name
def get_playlist_id(self, playlist_uri):
# Only A-Za-z0-9_ is allowed, which is 63 chars, so we can't use
# base64. Luckily, D-Bus does not limit the length of object paths.
# Since base32 pads trailing bytes with "=" chars, we need to replace
# them with an allowed character such as "_".
encoded_uri = base64.b32encode(playlist_uri).replace('=', '_')
return '/com/mopidy/playlist/%s' % encoded_uri
def get_playlist_uri(self, playlist_id):
encoded_uri = playlist_id.split('/')[-1].replace('_', '=')
return base64.b32decode(encoded_uri)
def get_track_id(self, tl_track):
return '/com/mopidy/track/%d' % tl_track.tlid
def get_track_tlid(self, track_id):
assert track_id.startswith('/com/mopidy/track/')
return track_id.split('/')[-1]
### Properties interface
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
in_signature='ss', out_signature='v')
def Get(self, interface, prop):
logger.debug(
'%s.Get(%s, %s) called',
dbus.PROPERTIES_IFACE, repr(interface), repr(prop))
(getter, _) = self.properties[interface][prop]
if callable(getter):
return getter()
else:
return getter
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
in_signature='s', out_signature='a{sv}')
def GetAll(self, interface):
logger.debug(
'%s.GetAll(%s) called', dbus.PROPERTIES_IFACE, repr(interface))
getters = {}
for key, (getter, _) in self.properties[interface].iteritems():
getters[key] = getter() if callable(getter) else getter
return getters
@dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE,
in_signature='ssv', out_signature='')
def Set(self, interface, prop, value):
logger.debug(
'%s.Set(%s, %s, %s) called',
dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value))
_, setter = self.properties[interface][prop]
if setter is not None:
setter(value)
self.PropertiesChanged(
interface, {prop: self.Get(interface, prop)}, [])
@dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE,
signature='sa{sv}as')
def PropertiesChanged(self, interface, changed_properties,
invalidated_properties):
logger.debug(
'%s.PropertiesChanged(%s, %s, %s) signaled',
dbus.PROPERTIES_IFACE, interface, changed_properties,
invalidated_properties)
### Root interface methods
@dbus.service.method(dbus_interface=ROOT_IFACE)
def Raise(self):
logger.debug('%s.Raise called', ROOT_IFACE)
# Do nothing, as we do not have a GUI
@dbus.service.method(dbus_interface=ROOT_IFACE)
def Quit(self):
logger.debug('%s.Quit called', ROOT_IFACE)
exit_process()
### Root interface properties
def get_DesktopEntry(self):
return os.path.splitext(os.path.basename(
self.config['mpris']['desktop_file']))[0]
def get_SupportedUriSchemes(self):
return dbus.Array(self.core.uri_schemes.get(), signature='s')
### Player interface methods
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Next(self):
logger.debug('%s.Next called', PLAYER_IFACE)
if not self.get_CanGoNext():
logger.debug('%s.Next not allowed', PLAYER_IFACE)
return
self.core.playback.next().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Previous(self):
logger.debug('%s.Previous called', PLAYER_IFACE)
if not self.get_CanGoPrevious():
logger.debug('%s.Previous not allowed', PLAYER_IFACE)
return
self.core.playback.previous().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Pause(self):
logger.debug('%s.Pause called', PLAYER_IFACE)
if not self.get_CanPause():
logger.debug('%s.Pause not allowed', PLAYER_IFACE)
return
self.core.playback.pause().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def PlayPause(self):
logger.debug('%s.PlayPause called', PLAYER_IFACE)
if not self.get_CanPause():
logger.debug('%s.PlayPause not allowed', PLAYER_IFACE)
return
state = self.core.playback.state.get()
if state == PlaybackState.PLAYING:
self.core.playback.pause().get()
elif state == PlaybackState.PAUSED:
self.core.playback.resume().get()
elif state == PlaybackState.STOPPED:
self.core.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Stop(self):
logger.debug('%s.Stop called', PLAYER_IFACE)
if not self.get_CanControl():
logger.debug('%s.Stop not allowed', PLAYER_IFACE)
return
self.core.playback.stop().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Play(self):
logger.debug('%s.Play called', PLAYER_IFACE)
if not self.get_CanPlay():
logger.debug('%s.Play not allowed', PLAYER_IFACE)
return
state = self.core.playback.state.get()
if state == PlaybackState.PAUSED:
self.core.playback.resume().get()
else:
self.core.playback.play().get()
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def Seek(self, offset):
logger.debug('%s.Seek called', PLAYER_IFACE)
if not self.get_CanSeek():
logger.debug('%s.Seek not allowed', PLAYER_IFACE)
return
offset_in_milliseconds = offset // 1000
current_position = self.core.playback.time_position.get()
new_position = current_position + offset_in_milliseconds
self.core.playback.seek(new_position)
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def SetPosition(self, track_id, position):
logger.debug('%s.SetPosition called', PLAYER_IFACE)
if not self.get_CanSeek():
logger.debug('%s.SetPosition not allowed', PLAYER_IFACE)
return
position = position // 1000
current_tl_track = self.core.playback.current_tl_track.get()
if current_tl_track is None:
return
if track_id != self.get_track_id(current_tl_track):
return
if position < 0:
return
if current_tl_track.track.length < position:
return
self.core.playback.seek(position)
@dbus.service.method(dbus_interface=PLAYER_IFACE)
def OpenUri(self, uri):
logger.debug('%s.OpenUri called', PLAYER_IFACE)
if not self.get_CanPlay():
# NOTE The spec does not explictly require this check, but guarding
# the other methods doesn't help much if OpenUri is open for use.
logger.debug('%s.Play not allowed', PLAYER_IFACE)
return
# NOTE Check if URI has MIME type known to the backend, if MIME support
# is added to the backend.
tl_tracks = self.core.tracklist.add(uri=uri).get()
if tl_tracks:
self.core.playback.play(tl_tracks[0])
else:
logger.debug('Track with URI "%s" not found in library.', uri)
### Player interface signals
@dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x')
def Seeked(self, position):
logger.debug('%s.Seeked signaled', PLAYER_IFACE)
# Do nothing, as just calling the method is enough to emit the signal.
### Player interface properties
def get_PlaybackStatus(self):
state = self.core.playback.state.get()
if state == PlaybackState.PLAYING:
return 'Playing'
elif state == PlaybackState.PAUSED:
return 'Paused'
elif state == PlaybackState.STOPPED:
return 'Stopped'
def get_LoopStatus(self):
repeat = self.core.playback.repeat.get()
single = self.core.playback.single.get()
if not repeat:
return 'None'
else:
if single:
return 'Track'
else:
return 'Playlist'
def set_LoopStatus(self, value):
if not self.get_CanControl():
logger.debug('Setting %s.LoopStatus not allowed', PLAYER_IFACE)
return
if value == 'None':
self.core.playback.repeat = False
self.core.playback.single = False
elif value == 'Track':
self.core.playback.repeat = True
self.core.playback.single = True
elif value == 'Playlist':
self.core.playback.repeat = True
self.core.playback.single = False
def set_Rate(self, value):
if not self.get_CanControl():
# NOTE The spec does not explictly require this check, but it was
# added to be consistent with all the other property setters.
logger.debug('Setting %s.Rate not allowed', PLAYER_IFACE)
return
if value == 0:
self.Pause()
def get_Shuffle(self):
return self.core.playback.random.get()
def set_Shuffle(self, value):
if not self.get_CanControl():
logger.debug('Setting %s.Shuffle not allowed', PLAYER_IFACE)
return
if value:
self.core.playback.random = True
else:
self.core.playback.random = False
def get_Metadata(self):
current_tl_track = self.core.playback.current_tl_track.get()
if current_tl_track is None:
return {'mpris:trackid': ''}
else:
(_, track) = current_tl_track
metadata = {'mpris:trackid': self.get_track_id(current_tl_track)}
if track.length:
metadata['mpris:length'] = track.length * 1000
if track.uri:
metadata['xesam:url'] = track.uri
if track.name:
metadata['xesam:title'] = track.name
if track.artists:
artists = list(track.artists)
artists.sort(key=lambda a: a.name)
metadata['xesam:artist'] = dbus.Array(
[a.name for a in artists if a.name], signature='s')
if track.album and track.album.name:
metadata['xesam:album'] = track.album.name
if track.album and track.album.artists:
artists = list(track.album.artists)
artists.sort(key=lambda a: a.name)
metadata['xesam:albumArtist'] = dbus.Array(
[a.name for a in artists if a.name], signature='s')
if track.album and track.album.images:
url = list(track.album.images)[0]
if url:
metadata['mpris:artUrl'] = url
if track.disc_no:
metadata['xesam:discNumber'] = track.disc_no
if track.track_no:
metadata['xesam:trackNumber'] = track.track_no
return dbus.Dictionary(metadata, signature='sv')
def get_Volume(self):
volume = self.core.playback.volume.get()
if volume is None:
return 0
return volume / 100.0
def set_Volume(self, value):
if not self.get_CanControl():
logger.debug('Setting %s.Volume not allowed', PLAYER_IFACE)
return
if value is None:
return
elif value < 0:
self.core.playback.volume = 0
elif value > 1:
self.core.playback.volume = 100
elif 0 <= value <= 1:
self.core.playback.volume = int(value * 100)
def get_Position(self):
return self.core.playback.time_position.get() * 1000
def get_CanGoNext(self):
if not self.get_CanControl():
return False
return (
self.core.playback.tl_track_at_next.get() !=
self.core.playback.current_tl_track.get())
def get_CanGoPrevious(self):
if not self.get_CanControl():
return False
return (
self.core.playback.tl_track_at_previous.get() !=
self.core.playback.current_tl_track.get())
def get_CanPlay(self):
if not self.get_CanControl():
return False
return (
self.core.playback.current_tl_track.get() is not None or
self.core.playback.tl_track_at_next.get() is not None)
def get_CanPause(self):
if not self.get_CanControl():
return False
# NOTE Should be changed to vary based on capabilities of the current
# track if Mopidy starts supporting non-seekable media, like streams.
return True
def get_CanSeek(self):
if not self.get_CanControl():
return False
# NOTE Should be changed to vary based on capabilities of the current
# track if Mopidy starts supporting non-seekable media, like streams.
return True
def get_CanControl(self):
# NOTE This could be a setting for the end user to change.
return True
### Playlists interface methods
@dbus.service.method(dbus_interface=PLAYLISTS_IFACE)
def ActivatePlaylist(self, playlist_id):
logger.debug(
'%s.ActivatePlaylist(%r) called', PLAYLISTS_IFACE, playlist_id)
playlist_uri = self.get_playlist_uri(playlist_id)
playlist = self.core.playlists.lookup(playlist_uri).get()
if playlist and playlist.tracks:
tl_tracks = self.core.tracklist.add(playlist.tracks).get()
self.core.playback.play(tl_tracks[0])
@dbus.service.method(dbus_interface=PLAYLISTS_IFACE)
def GetPlaylists(self, index, max_count, order, reverse):
logger.debug(
'%s.GetPlaylists(%r, %r, %r, %r) called',
PLAYLISTS_IFACE, index, max_count, order, reverse)
playlists = self.core.playlists.playlists.get()
if order == 'Alphabetical':
playlists.sort(key=lambda p: p.name, reverse=reverse)
elif order == 'Modified':
playlists.sort(key=lambda p: p.last_modified, reverse=reverse)
elif order == 'User' and reverse:
playlists.reverse()
slice_end = index + max_count
playlists = playlists[index:slice_end]
results = [
(self.get_playlist_id(p.uri), p.name, '')
for p in playlists]
return dbus.Array(results, signature='(oss)')
### Playlists interface signals
@dbus.service.signal(dbus_interface=PLAYLISTS_IFACE, signature='(oss)')
def PlaylistChanged(self, playlist):
logger.debug('%s.PlaylistChanged signaled', PLAYLISTS_IFACE)
# Do nothing, as just calling the method is enough to emit the signal.
### Playlists interface properties
def get_PlaylistCount(self):
return len(self.core.playlists.playlists.get())
def get_Orderings(self):
return [
'Alphabetical', # Order by playlist.name
'Modified', # Order by playlist.last_modified
'User', # Don't change order
]
def get_ActivePlaylist(self):
playlist_is_valid = False
playlist = ('/', 'None', '')
return (playlist_is_valid, playlist)

View File

@ -1,33 +0,0 @@
from __future__ import unicode_literals
import os
import mopidy
from mopidy import config, exceptions, ext
class Extension(ext.Extension):
dist_name = 'Mopidy-Scrobbler'
ext_name = 'scrobbler'
version = mopidy.__version__
def get_default_config(self):
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
return config.read(conf_file)
def get_config_schema(self):
schema = super(Extension, self).get_config_schema()
schema['username'] = config.String()
schema['password'] = config.Secret()
return schema
def validate_environment(self):
try:
import pylast # noqa
except ImportError as e:
raise exceptions.ExtensionError('pylast library not found', e)
def get_frontend_classes(self):
from .actor import ScrobblerFrontend
return [ScrobblerFrontend]

View File

@ -1,81 +0,0 @@
from __future__ import unicode_literals
import logging
import time
import pykka
import pylast
from mopidy.core import CoreListener
logger = logging.getLogger('mopidy.frontends.scrobbler')
API_KEY = '2236babefa8ebb3d93ea467560d00d04'
API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd'
class ScrobblerFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super(ScrobblerFrontend, self).__init__()
self.config = config
self.lastfm = None
self.last_start_time = None
def on_start(self):
try:
self.lastfm = pylast.LastFMNetwork(
api_key=API_KEY, api_secret=API_SECRET,
username=self.config['scrobbler']['username'],
password_hash=pylast.md5(self.config['scrobbler']['password']))
logger.info('Scrobbler connected to Last.fm')
except (pylast.NetworkError, pylast.MalformedResponseError,
pylast.WSError) as e:
logger.error('Error during Last.fm setup: %s', e)
self.stop()
def track_playback_started(self, tl_track):
track = tl_track.track
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
self.last_start_time = int(time.time())
logger.debug('Now playing track: %s - %s', artists, track.name)
try:
self.lastfm.update_now_playing(
artists,
(track.name or ''),
album=(track.album and track.album.name or ''),
duration=str(duration),
track_number=str(track.track_no),
mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, pylast.NetworkError,
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning('Error submitting playing track to Last.fm: %s', e)
def track_playback_ended(self, tl_track, time_position):
track = tl_track.track
artists = ', '.join([a.name for a in track.artists])
duration = track.length and track.length // 1000 or 0
time_position = time_position // 1000
if duration < 30:
logger.debug('Track too short to scrobble. (30s)')
return
if time_position < duration // 2 and time_position < 240:
logger.debug(
'Track not played long enough to scrobble. (50% or 240s)')
return
if self.last_start_time is None:
self.last_start_time = int(time.time()) - duration
logger.debug('Scrobbling track: %s - %s', artists, track.name)
try:
self.lastfm.scrobble(
artists,
(track.name or ''),
str(self.last_start_time),
album=(track.album and track.album.name or ''),
track_number=str(track.track_no),
duration=str(duration),
mbid=(track.musicbrainz_id or ''))
except (pylast.ScrobblingError, pylast.NetworkError,
pylast.MalformedResponseError, pylast.WSError) as e:
logger.warning('Error submitting played track to Last.fm: %s', e)

View File

@ -1,4 +0,0 @@
[scrobbler]
enabled = true
username =
password =

View File

@ -16,17 +16,12 @@ mopidy_args = sys.argv[1:]
sys.argv[1:] = []
# Add ../ to the path so we can run Mopidy from a Git checkout without
# installing it on the system.
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../')))
import pygst
pygst.require('0.10')
import gst
import gst.pbutils
from mopidy import config as config_lib, ext
from mopidy import config as config_lib, exceptions, ext
from mopidy.models import Track, Artist, Album
from mopidy.utils import log, path, versioning
@ -76,6 +71,7 @@ def main():
local_updater = updaters.values()[0](config) # TODO: switch to actor?
media_dir = config['local']['media_dir']
excluded_extensions = config['local']['excluded_file_extensions']
uris_library = set()
uris_update = set()
@ -97,30 +93,26 @@ def main():
logging.info('Checking %s for new or modified tracks.', media_dir)
for uri in path.find_uris(config['local']['media_dir']):
if os.path.splitext(path.uri_to_path(uri))[1] in excluded_extensions:
logging.debug('Skipped %s: File extension excluded.', uri)
continue
if uri not in uris_library:
uris_update.add(uri)
logging.info('Found %d new or modified tracks.', len(uris_update))
def store(data):
track = translator(data)
local_updater.add(track)
logging.debug('Added %s', track.uri)
def debug(uri, error, debug):
logging.warning('Failed %s: %s', uri, error)
logging.debug('Debug info for %s: %s', uri, debug)
scan_timeout = config['local']['scan_timeout']
logging.info('Scanning new and modified tracks.')
# TODO: just pass the library in instead?
scanner = Scanner(uris_update, store, debug, scan_timeout)
try:
scanner.start()
except KeyboardInterrupt:
scanner.stop()
raise
scanner = Scanner(config['local']['scan_timeout'])
for uri in uris_update:
try:
data = scanner.scan(uri)
data[b'mtime'] = os.path.getmtime(path.uri_to_path(uri))
track = translator(data)
local_updater.add(track)
logging.debug('Added %s', track.uri)
except exceptions.ScannerError as error:
logging.warning('Failed %s: %s', uri, error)
logging.info('Done scanning; commiting changes.')
local_updater.commit()
@ -192,125 +184,44 @@ def translator(data):
class Scanner(object):
def __init__(
self, uris, data_callback, error_callback=None, scan_timeout=1000):
self.data = {}
self.uris = iter(uris)
self.data_callback = data_callback
self.error_callback = error_callback
self.scan_timeout = scan_timeout
self.loop = gobject.MainLoop()
self.timeout_id = None
self.fakesink = gst.element_factory_make('fakesink')
self.fakesink.set_property('signal-handoffs', True)
self.fakesink.connect('handoff', self.process_handoff)
self.uribin = gst.element_factory_make('uridecodebin')
self.uribin.set_property(
'caps', gst.Caps(b'audio/x-raw-int; audio/x-raw-float'))
self.uribin.connect('pad-added', self.process_new_pad)
self.pipe = gst.element_factory_make('pipeline')
self.pipe.add(self.uribin)
self.pipe.add(self.fakesink)
bus = self.pipe.get_bus()
bus.add_signal_watch()
bus.connect('message::application', self.process_application)
bus.connect('message::tag', self.process_tags)
bus.connect('message::error', self.process_error)
def process_handoff(self, fakesink, buffer_, pad):
# When this function is called the first buffer has reached the end of
# the pipeline, and we can continue with the next track. Since we're
# in another thread, we send a message back to the main thread using
# the bus.
structure = gst.Structure('handoff')
message = gst.message_new_application(fakesink, structure)
bus = self.pipe.get_bus()
bus.post(message)
def process_new_pad(self, source, pad):
pad.link(self.fakesink.get_pad('sink'))
def process_application(self, bus, message):
if message.src != self.fakesink:
return
if message.structure.get_name() != 'handoff':
return
uri = unicode(self.uribin.get_property('uri'))
self.data['uri'] = uri
self.data['mtime'] = os.path.getmtime(path.uri_to_path(uri))
self.data[gst.TAG_DURATION] = self.get_duration()
def __init__(self, timeout=1000):
self.discoverer = gst.pbutils.Discoverer(timeout * 1000000)
def scan(self, uri):
try:
self.data_callback(self.data)
self.next_uri()
except KeyboardInterrupt:
self.stop()
info = self.discoverer.discover_uri(uri)
except gobject.GError as e:
# Loosing traceback is non-issue since this is from C code.
raise exceptions.ScannerError(e)
def process_tags(self, bus, message):
taglist = message.parse_tag()
data = {}
audio_streams = info.get_audio_streams()
for key in taglist.keys():
# XXX: For some crazy reason some wma files spit out lists here,
# not sure if this is due to better data in headers or wma being
# stupid. So ugly hack for now :/
if type(taglist[key]) is list:
self.data[key] = taglist[key][0]
else:
self.data[key] = taglist[key]
if not audio_streams:
raise exceptions.ScannerError('Did not find any audio streams.')
def process_error(self, bus, message):
if self.error_callback:
uri = self.uribin.get_property('uri')
error, debug = message.parse_error()
self.error_callback(uri, error, debug)
self.next_uri()
for stream in audio_streams:
taglist = stream.get_tags()
if not taglist:
continue
for key in taglist.keys():
# XXX: For some crazy reason some wma files spit out lists
# here, not sure if this is due to better data in headers or
# wma being stupid. So ugly hack for now :/
if type(taglist[key]) is list:
data[key] = taglist[key][0]
else:
data[key] = taglist[key]
def process_timeout(self):
if self.error_callback:
uri = self.uribin.get_property('uri')
self.error_callback(
uri, 'Scan timed out after %d ms' % self.scan_timeout, None)
self.next_uri()
return False
# Never trust metadata for these fields:
data[b'uri'] = uri
data[b'duration'] = info.get_duration() // gst.MSECOND
def get_duration(self):
self.pipe.get_state() # Block until state change is done.
try:
return self.pipe.query_duration(
gst.FORMAT_TIME, None)[0] // gst.MSECOND
except gst.QueryError:
return None
if data[b'duration'] < 100:
raise exceptions.ScannerError(
'Rejecting file with less than 100ms audio data.')
def next_uri(self):
self.data = {}
if self.timeout_id:
gobject.source_remove(self.timeout_id)
self.timeout_id = None
try:
uri = next(self.uris)
except StopIteration:
self.stop()
return False
self.pipe.set_state(gst.STATE_NULL)
self.uribin.set_property('uri', uri)
self.timeout_id = gobject.timeout_add(
self.scan_timeout, self.process_timeout)
self.pipe.set_state(gst.STATE_PLAYING)
return True
def start(self):
if self.next_uri():
self.loop.run()
def stop(self):
self.pipe.set_state(gst.STATE_NULL)
self.loop.quit()
return data
if __name__ == '__main__':

View File

@ -1,3 +0,0 @@
pylast >= 0.5.7
# Available as python-pylast in newer Debian/Ubuntu and from apt.mopidy.com for
# older releases of Debian/Ubuntu

View File

@ -1,8 +0,0 @@
pyspotify >= 1.9, < 2
# The libspotify Python wrapper
# Available as the python-spotify package from apt.mopidy.com
# libspotify >= 12, < 13
# The libspotify C library from
# https://developer.spotify.com/technologies/libspotify/
# Available as the libspotify12 package from apt.mopidy.com

View File

@ -28,8 +28,6 @@ setup(
'Pykka >= 1.1',
],
extras_require={
'spotify': ['pyspotify >= 1.9, < 2'],
'scrobbler': ['pylast >= 0.5.7'],
'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'],
},
test_suite='nose.collector',
@ -45,11 +43,8 @@ setup(
],
'mopidy.ext': [
'http = mopidy.frontends.http:Extension [http]',
'scrobbler = mopidy.frontends.scrobbler:Extension [scrobbler]',
'local = mopidy.backends.local:Extension',
'mpd = mopidy.frontends.mpd:Extension',
'mpris = mopidy.frontends.mpris:Extension',
'spotify = mopidy.backends.spotify:Extension [spotify]',
'stream = mopidy.backends.stream:Extension',
],
},

View File

@ -6,6 +6,9 @@ import pygst
pygst.require('0.10')
import gst
import gobject
gobject.threads_init()
import pykka
from mopidy import audio
@ -80,6 +83,22 @@ class AudioTest(unittest.TestCase):
self.assertTrue(self.audio.set_volume(value).get())
self.assertEqual(value, self.audio.get_volume().get())
def test_set_volume_with_mixer_min_equal_max(self):
config = {
'audio': {
'mixer': 'fakemixer track_max_volume=0',
'mixer_track': None,
'output': 'fakesink',
'visualizer': None,
}
}
self.audio = audio.Audio.start(config=config).proxy()
self.assertEqual(0, self.audio.get_volume().get())
@unittest.SkipTest
def test_set_mute(self):
pass # TODO Probably needs a fakemixer with a mixer track
@unittest.SkipTest
def test_set_state_encapsulation(self):
pass # TODO

View File

@ -0,0 +1,128 @@
#encoding: utf-8
from __future__ import unicode_literals
import io
import unittest
from mopidy.audio import playlists
BAD = b'foobarbaz'
M3U = b"""#EXTM3U
#EXTINF:123, Sample artist - Sample title
file:///tmp/foo
#EXTINF:321,Example Artist - Example title
file:///tmp/bar
#EXTINF:213,Some Artist - Other title
file:///tmp/baz
"""
PLS = b"""[Playlist]
NumberOfEntries=3
File1=file:///tmp/foo
Title1=Sample Title
Length1=123
File2=file:///tmp/bar
Title2=Example title
Length2=321
File3=file:///tmp/baz
Title3=Other title
Length3=213
Version=2
"""
ASX = b"""<asx version="3.0">
<title>Example</title>
<entry>
<title>Sample Title</title>
<ref href="file:///tmp/foo" />
</entry>
<entry>
<title>Example title</title>
<ref href="file:///tmp/bar" />
</entry>
<entry>
<title>Other title</title>
<ref href="file:///tmp/baz" />
</entry>
</asx>
"""
XSPF = b"""<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<trackList>
<track>
<title>Sample Title</title>
<location>file:///tmp/foo</location>
</track>
<track>
<title>Example title</title>
<location>file:///tmp/bar</location>
</track>
<track>
<title>Other title</title>
<location>file:///tmp/baz</location>
</track>
</trackList>
</playlist>
"""
class TypeFind(object):
def __init__(self, data):
self.data = data
def peek(self, start, end):
return self.data[start:end]
class BasePlaylistTest(object):
valid = None
invalid = None
detect = None
parse = None
def test_detect_valid_header(self):
self.assertTrue(self.detect(TypeFind(self.valid)))
def test_detect_invalid_header(self):
self.assertFalse(self.detect(TypeFind(self.invalid)))
def test_parse_valid_playlist(self):
uris = list(self.parse(io.BytesIO(self.valid)))
expected = [b'file:///tmp/foo', b'file:///tmp/bar', b'file:///tmp/baz']
self.assertEqual(uris, expected)
def test_parse_invalid_playlist(self):
uris = list(self.parse(io.BytesIO(self.invalid)))
self.assertEqual(uris, [])
class M3uPlaylistTest(BasePlaylistTest, unittest.TestCase):
valid = M3U
invalid = BAD
detect = staticmethod(playlists.detect_m3u_header)
parse = staticmethod(playlists.parse_m3u)
class PlsPlaylistTest(BasePlaylistTest, unittest.TestCase):
valid = PLS
invalid = BAD
detect = staticmethod(playlists.detect_pls_header)
parse = staticmethod(playlists.parse_pls)
class AsxPlsPlaylistTest(BasePlaylistTest, unittest.TestCase):
valid = ASX
invalid = BAD
detect = staticmethod(playlists.detect_asx_header)
parse = staticmethod(playlists.parse_asx)
class XspfPlaylistTest(BasePlaylistTest, unittest.TestCase):
valid = XSPF
invalid = BAD
detect = staticmethod(playlists.detect_xspf_header)
parse = staticmethod(playlists.parse_xspf)

View File

@ -1,11 +0,0 @@
from __future__ import unicode_literals
def populate_tracklist(func):
def wrapper(self):
self.tl_tracks = self.core.tracklist.add(self.tracks)
return func(self)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper

View File

@ -1,26 +0,0 @@
from __future__ import unicode_literals
import mock
import pykka
from mopidy import core, audio
from mopidy.backends import listener
@mock.patch.object(listener.BackendListener, 'send')
class BackendEventsTest(object):
config = {}
def setUp(self):
self.audio = audio.DummyAudio.start().proxy()
self.backend = self.backend_class.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
send.reset_mock()
self.core.playlists.refresh().get()
self.assertEqual(send.call_args[0][0], 'playlists_loaded')

View File

@ -1,202 +0,0 @@
from __future__ import unicode_literals
import unittest
import pykka
from mopidy import core
from mopidy.models import Track, Album, Artist
class LibraryControllerTest(object):
artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()]
albums = [
Album(name='album1', artists=artists[:1]),
Album(name='album2', artists=artists[1:2]),
Album()]
tracks = [
Track(uri='local:track:path1', name='track1', artists=artists[:1],
album=albums[0], date='2001-02-03', length=4000),
Track(uri='local:track:path2', name='track2', artists=artists[1:2],
album=albums[1], date='2002', length=4000),
Track()]
config = {}
def setUp(self):
self.backend = self.backend_class.start(
config=self.config, audio=None).proxy()
self.core = core.Core(backends=[self.backend])
self.library = self.core.library
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_refresh(self):
self.library.refresh()
@unittest.SkipTest
def test_refresh_uri(self):
pass
@unittest.SkipTest
def test_refresh_missing_uri(self):
pass
def test_lookup(self):
tracks = self.library.lookup(self.tracks[0].uri)
self.assertEqual(tracks, self.tracks[0:1])
def test_lookup_unknown_track(self):
tracks = self.library.lookup('fake uri')
self.assertEqual(tracks, [])
def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(artist=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(album=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_uri(self):
track_1_uri = 'local:track:path1'
result = self.library.find_exact(uri=track_1_uri)
self.assertEqual(list(result[0].tracks), self.tracks[:1])
track_2_uri = 'local:track:path2'
result = self.library.find_exact(uri=track_2_uri)
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_track(self):
result = self.library.find_exact(track=['track1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(track=['track2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_artist(self):
result = self.library.find_exact(artist=['artist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(artist=['artist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_album(self):
result = self.library.find_exact(album=['album1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(album=['album2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_date(self):
result = self.library.find_exact(date=['2001'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(date=['2001-02-03'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(date=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_wrong_type(self):
test = lambda: self.library.find_exact(wrong=['test'])
self.assertRaises(LookupError, test)
def test_find_exact_with_empty_query(self):
test = lambda: self.library.find_exact(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(album=[''])
self.assertRaises(LookupError, test)
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(artist=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(uri=['unknown'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(any=['unknown'])
self.assertEqual(list(result[0].tracks), [])
def test_search_uri(self):
result = self.library.search(uri=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(uri=['TH2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self):
result = self.library.search(track=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(track=['Rack2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_artist(self):
result = self.library.search(artist=['Tist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(artist=['Tist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_album(self):
result = self.library.search(album=['Bum1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(album=['Bum2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_date(self):
result = self.library.search(date=['2001'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-03'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-04'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(date=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_any(self):
result = self.library.search(any=['Tist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['Bum1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test'])
self.assertRaises(LookupError, test)
def test_search_with_empty_query(self):
test = lambda: self.library.search(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(uri=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(any=[''])
self.assertRaises(LookupError, test)

View File

@ -1,876 +0,0 @@
from __future__ import unicode_literals
import mock
import random
import time
import unittest
import pykka
from mopidy import audio, core
from mopidy.core import PlaybackState
from mopidy.models import Track
from tests.backends.base import populate_tracklist
# TODO Test 'playlist repeat', e.g. repeat=1,single=0
class PlaybackControllerTest(object):
tracks = []
config = {}
def setUp(self):
self.audio = audio.DummyAudio.start().proxy()
self.backend = self.backend_class.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core(backends=[self.backend])
self.playback = self.core.playback
self.tracklist = self.core.tracklist
assert len(self.tracks) >= 3, \
'Need at least three tracks to run tests.'
assert self.tracks[0].length >= 2000, \
'First song needs to be at least 2000 miliseconds'
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_initial_state_is_stopped(self):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_play_with_empty_playlist(self):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_play_with_empty_playlist_return_value(self):
self.assertEqual(self.playback.play(), None)
@populate_tracklist
def test_play_state(self):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_play_return_value(self):
self.assertEqual(self.playback.play(), None)
@populate_tracklist
def test_play_track_state(self):
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play(self.tracklist.tl_tracks[-1])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_play_track_return_value(self):
self.assertEqual(self.playback.play(
self.tracklist.tl_tracks[-1]), None)
@populate_tracklist
def test_play_when_playing(self):
self.playback.play()
track = self.playback.current_track
self.playback.play()
self.assertEqual(track, self.playback.current_track)
@populate_tracklist
def test_play_when_paused(self):
self.playback.play()
track = self.playback.current_track
self.playback.pause()
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(track, self.playback.current_track)
@populate_tracklist
def test_play_when_pause_after_next(self):
self.playback.play()
self.playback.next()
self.playback.next()
track = self.playback.current_track
self.playback.pause()
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(track, self.playback.current_track)
@populate_tracklist
def test_play_sets_current_track(self):
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_tracklist
def test_play_track_sets_current_track(self):
self.playback.play(self.tracklist.tl_tracks[-1])
self.assertEqual(self.playback.current_track, self.tracks[-1])
@populate_tracklist
def test_play_skips_to_next_track_on_failure(self):
# If backend's play() returns False, it is a failure.
self.backend.playback.play = lambda track: track != self.tracks[0]
self.playback.play()
self.assertNotEqual(self.playback.current_track, self.tracks[0])
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_current_track_after_completed_playlist(self):
self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_tracklist
def test_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_tracklist
def test_previous_more(self):
self.playback.play() # At track 0
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_previous_return_value(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.previous(), None)
@populate_tracklist
def test_previous_does_not_trigger_playback(self):
self.playback.play()
self.playback.next()
self.playback.stop()
self.playback.previous()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_previous_at_start_of_playlist(self):
self.playback.previous()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
def test_previous_for_empty_playlist(self):
self.playback.previous()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_tracklist
def test_previous_skips_to_previous_track_on_failure(self):
# If backend's play() returns False, it is a failure.
self.backend.playback.play = lambda track: track != self.tracks[1]
self.playback.play(self.tracklist.tl_tracks[2])
self.assertEqual(self.playback.current_track, self.tracks[2])
self.playback.previous()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_tracklist
def test_next(self):
self.playback.play()
old_position = self.playback.tracklist_position
old_uri = self.playback.current_track.uri
self.playback.next()
self.assertEqual(
self.playback.tracklist_position, old_position + 1)
self.assertNotEqual(self.playback.current_track.uri, old_uri)
@populate_tracklist
def test_next_return_value(self):
self.playback.play()
self.assertEqual(self.playback.next(), None)
@populate_tracklist
def test_next_does_not_trigger_playback(self):
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_next_at_end_of_playlist(self):
self.playback.play()
for i, track in enumerate(self.tracks):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
self.assertEqual(self.playback.tracklist_position, i)
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_next_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
for _ in self.tracks:
self.playback.next()
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, self.tracks[0])
def test_next_for_empty_playlist(self):
self.playback.next()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_next_skips_to_next_track_on_failure(self):
# If backend's play() returns False, it is a failure.
self.backend.playback.play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.next()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_tracklist
def test_next_track_before_play(self):
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
@populate_tracklist
def test_next_track_during_play(self):
self.playback.play()
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
@populate_tracklist
def test_next_track_after_previous(self):
self.playback.play()
self.playback.next()
self.playback.previous()
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
def test_next_track_empty_playlist(self):
self.assertEqual(self.playback.tl_track_at_next, None)
@populate_tracklist
def test_next_track_at_end_of_playlist(self):
self.playback.play()
for _ in self.tracklist.tl_tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.tl_track_at_next, None)
@populate_tracklist
def test_next_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for _ in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
@populate_tracklist
def test_next_track_with_random(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
@populate_tracklist
def test_next_with_consume(self):
self.playback.consume = True
self.playback.play()
self.playback.next()
self.assertIn(self.tracks[0], self.tracklist.tracks)
@populate_tracklist
def test_next_with_single_and_repeat(self):
self.playback.single = True
self.playback.repeat = True
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_next_with_random(self):
# FIXME feels very fragile
random.seed(1)
self.playback.random = True
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_next_track_with_random_after_append_playlist(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
self.tracklist.add(self.tracks[:1])
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
@populate_tracklist
def test_end_of_track(self):
self.playback.play()
old_position = self.playback.tracklist_position
old_uri = self.playback.current_track.uri
self.playback.on_end_of_track()
self.assertEqual(
self.playback.tracklist_position, old_position + 1)
self.assertNotEqual(self.playback.current_track.uri, old_uri)
@populate_tracklist
def test_end_of_track_return_value(self):
self.playback.play()
self.assertEqual(self.playback.on_end_of_track(), None)
@populate_tracklist
def test_end_of_track_does_not_trigger_playback(self):
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_end_of_track_at_end_of_playlist(self):
self.playback.play()
for i, track in enumerate(self.tracks):
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
self.assertEqual(self.playback.tracklist_position, i)
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_end_of_track_until_end_of_playlist_and_play_from_start(self):
self.playback.play()
for _ in self.tracks:
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, None)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, self.tracks[0])
def test_end_of_track_for_empty_playlist(self):
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_end_of_track_skips_to_next_track_on_failure(self):
# If backend's play() returns False, it is a failure.
self.backend.playback.play = lambda track: track != self.tracks[1]
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
self.playback.on_end_of_track()
self.assertNotEqual(self.playback.current_track, self.tracks[1])
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_tracklist
def test_end_of_track_track_before_play(self):
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
@populate_tracklist
def test_end_of_track_track_during_play(self):
self.playback.play()
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
@populate_tracklist
def test_end_of_track_track_after_previous(self):
self.playback.play()
self.playback.on_end_of_track()
self.playback.previous()
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
def test_end_of_track_track_empty_playlist(self):
self.assertEqual(self.playback.tl_track_at_next, None)
@populate_tracklist
def test_end_of_track_track_at_end_of_playlist(self):
self.playback.play()
for _ in self.tracklist.tl_tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.tl_track_at_next, None)
@populate_tracklist
def test_end_of_track_track_at_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.play()
for _ in self.tracks[1:]:
self.playback.on_end_of_track()
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[0])
@populate_tracklist
def test_end_of_track_track_with_random(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
@populate_tracklist
def test_end_of_track_with_consume(self):
self.playback.consume = True
self.playback.play()
self.playback.on_end_of_track()
self.assertNotIn(self.tracks[0], self.tracklist.tracks)
@populate_tracklist
def test_end_of_track_with_random(self):
# FIXME feels very fragile
random.seed(1)
self.playback.random = True
self.playback.play()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_end_of_track_track_with_random_after_append_playlist(self):
random.seed(1)
self.playback.random = True
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[2])
self.tracklist.add(self.tracks[:1])
self.assertEqual(self.playback.tl_track_at_next, self.tl_tracks[1])
@populate_tracklist
def test_previous_track_before_play(self):
self.assertEqual(self.playback.tl_track_at_previous, None)
@populate_tracklist
def test_previous_track_after_play(self):
self.playback.play()
self.assertEqual(self.playback.tl_track_at_previous, None)
@populate_tracklist
def test_previous_track_after_next(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.tl_track_at_previous, self.tl_tracks[0])
@populate_tracklist
def test_previous_track_after_previous(self):
self.playback.play() # At track 0
self.playback.next() # At track 1
self.playback.next() # At track 2
self.playback.previous() # At track 1
self.assertEqual(self.playback.tl_track_at_previous, self.tl_tracks[0])
def test_previous_track_empty_playlist(self):
self.assertEqual(self.playback.tl_track_at_previous, None)
@populate_tracklist
def test_previous_track_with_consume(self):
self.playback.consume = True
for _ in self.tracks:
self.playback.next()
self.assertEqual(
self.playback.tl_track_at_previous,
self.playback.current_tl_track)
@populate_tracklist
def test_previous_track_with_random(self):
self.playback.random = True
for _ in self.tracks:
self.playback.next()
self.assertEqual(
self.playback.tl_track_at_previous,
self.playback.current_tl_track)
@populate_tracklist
def test_initial_current_track(self):
self.assertEqual(self.playback.current_track, None)
@populate_tracklist
def test_current_track_during_play(self):
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_tracklist
def test_current_track_after_next(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_initial_tracklist_position(self):
self.assertEqual(self.playback.tracklist_position, None)
@populate_tracklist
def test_tracklist_position_during_play(self):
self.playback.play()
self.assertEqual(self.playback.tracklist_position, 0)
@populate_tracklist
def test_tracklist_position_after_next(self):
self.playback.play()
self.playback.next()
self.assertEqual(self.playback.tracklist_position, 1)
@populate_tracklist
def test_tracklist_position_at_end_of_playlist(self):
self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.on_end_of_track()
self.assertEqual(self.playback.tracklist_position, None)
def test_on_tracklist_change_gets_called(self):
callback = self.playback.on_tracklist_change
def wrapper():
wrapper.called = True
return callback()
wrapper.called = False
self.playback.on_tracklist_change = wrapper
self.tracklist.add([Track()])
self.assert_(wrapper.called)
@unittest.SkipTest # Blocks for 10ms
@populate_tracklist
def test_end_of_track_callback_gets_called(self):
self.playback.play()
result = self.playback.seek(self.tracks[0].length - 10)
self.assertTrue(result, 'Seek failed')
message = self.core_queue.get(True, 1)
self.assertEqual('end_of_track', message['command'])
@populate_tracklist
def test_on_tracklist_change_when_playing(self):
self.playback.play()
current_track = self.playback.current_track
self.tracklist.add([self.tracks[2]])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, current_track)
@populate_tracklist
def test_on_tracklist_change_when_stopped(self):
self.tracklist.add([self.tracks[2]])
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_tracklist
def test_on_tracklist_change_when_paused(self):
self.playback.play()
self.playback.pause()
current_track = self.playback.current_track
self.tracklist.add([self.tracks[2]])
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
self.assertEqual(self.playback.current_track, current_track)
@populate_tracklist
def test_pause_when_stopped(self):
self.playback.pause()
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
@populate_tracklist
def test_pause_when_playing(self):
self.playback.play()
self.playback.pause()
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
@populate_tracklist
def test_pause_when_paused(self):
self.playback.play()
self.playback.pause()
self.playback.pause()
self.assertEqual(self.playback.state, PlaybackState.PAUSED)
@populate_tracklist
def test_pause_return_value(self):
self.playback.play()
self.assertEqual(self.playback.pause(), None)
@populate_tracklist
def test_resume_when_stopped(self):
self.playback.resume()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_resume_when_playing(self):
self.playback.play()
self.playback.resume()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_resume_when_paused(self):
self.playback.play()
self.playback.pause()
self.playback.resume()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_resume_return_value(self):
self.playback.play()
self.playback.pause()
self.assertEqual(self.playback.resume(), None)
@unittest.SkipTest # Uses sleep and might not work with LocalBackend
@populate_tracklist
def test_resume_continues_from_right_position(self):
self.playback.play()
time.sleep(0.2)
self.playback.pause()
self.playback.resume()
self.assertNotEqual(self.playback.time_position, 0)
@populate_tracklist
def test_seek_when_stopped(self):
result = self.playback.seek(1000)
self.assert_(result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_when_stopped_updates_position(self):
self.playback.seek(1000)
position = self.playback.time_position
self.assertGreaterEqual(position, 990)
def test_seek_on_empty_playlist(self):
self.assertFalse(self.playback.seek(0))
def test_seek_on_empty_playlist_updates_position(self):
self.playback.seek(0)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_seek_when_stopped_triggers_play(self):
self.playback.seek(0)
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_seek_when_playing(self):
self.playback.play()
result = self.playback.seek(self.tracks[0].length - 1000)
self.assert_(result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_when_playing_updates_position(self):
length = self.tracklist.tracks[0].length
self.playback.play()
self.playback.seek(length - 1000)
position = self.playback.time_position
self.assertGreaterEqual(position, length - 1010)
@populate_tracklist
def test_seek_when_paused(self):
self.playback.play()
self.playback.pause()
result = self.playback.seek(self.tracks[0].length - 1000)
self.assert_(result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_when_paused_updates_position(self):
length = self.tracklist.tracks[0].length
self.playback.play()
self.playback.pause()
self.playback.seek(length - 1000)
position = self.playback.time_position
self.assertGreaterEqual(position, length - 1010)
@populate_tracklist
def test_seek_when_paused_triggers_play(self):
self.playback.play()
self.playback.pause()
self.playback.seek(0)
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@unittest.SkipTest
@populate_tracklist
def test_seek_beyond_end_of_song(self):
# FIXME need to decide return value
self.playback.play()
result = self.playback.seek(self.tracks[0].length * 100)
self.assert_(not result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_beyond_end_of_song_jumps_to_next_song(self):
self.playback.play()
self.playback.seek(self.tracks[0].length * 100)
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_seek_beyond_end_of_song_for_last_track(self):
self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.seek(self.tracklist.tracks[-1].length * 100)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@unittest.SkipTest
@populate_tracklist
def test_seek_beyond_start_of_song(self):
# FIXME need to decide return value
self.playback.play()
result = self.playback.seek(-1000)
self.assert_(not result, 'Seek return value was %s' % result)
@populate_tracklist
def test_seek_beyond_start_of_song_update_postion(self):
self.playback.play()
self.playback.seek(-1000)
position = self.playback.time_position
self.assertGreaterEqual(position, 0)
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_stop_when_stopped(self):
self.playback.stop()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_stop_when_playing(self):
self.playback.play()
self.playback.stop()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
@populate_tracklist
def test_stop_when_paused(self):
self.playback.play()
self.playback.pause()
self.playback.stop()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_stop_return_value(self):
self.playback.play()
self.assertEqual(self.playback.stop(), None)
def test_time_position_when_stopped(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
self.audio.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
@populate_tracklist
def test_time_position_when_stopped_with_playlist(self):
future = mock.Mock()
future.get = mock.Mock(return_value=0)
self.audio.get_position = mock.Mock(return_value=future)
self.assertEqual(self.playback.time_position, 0)
@unittest.SkipTest # Uses sleep and does might not work with LocalBackend
@populate_tracklist
def test_time_position_when_playing(self):
self.playback.play()
first = self.playback.time_position
time.sleep(1)
second = self.playback.time_position
self.assertGreater(second, first)
@unittest.SkipTest # Uses sleep
@populate_tracklist
def test_time_position_when_paused(self):
self.playback.play()
time.sleep(0.2)
self.playback.pause()
time.sleep(0.2)
first = self.playback.time_position
second = self.playback.time_position
self.assertEqual(first, second)
@populate_tracklist
def test_play_with_consume(self):
self.playback.consume = True
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_tracklist
def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self):
self.playback.consume = True
self.playback.play()
for _ in range(len(self.tracklist.tracks)):
self.playback.on_end_of_track()
self.assertEqual(len(self.tracklist.tracks), 0)
@populate_tracklist
def test_play_with_random(self):
random.seed(1)
self.playback.random = True
self.playback.play()
self.assertEqual(self.playback.current_track, self.tracks[2])
@populate_tracklist
def test_previous_with_random(self):
random.seed(1)
self.playback.random = True
self.playback.play()
self.playback.next()
current_track = self.playback.current_track
self.playback.previous()
self.assertEqual(self.playback.current_track, current_track)
@populate_tracklist
def test_end_of_song_starts_next_track(self):
self.playback.play()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[1])
@populate_tracklist
def test_end_of_song_with_single_and_repeat_starts_same(self):
self.playback.single = True
self.playback.repeat = True
self.playback.play()
self.playback.on_end_of_track()
self.assertEqual(self.playback.current_track, self.tracks[0])
@populate_tracklist
def test_end_of_playlist_stops(self):
self.playback.play(self.tracklist.tl_tracks[-1])
self.playback.on_end_of_track()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_repeat_off_by_default(self):
self.assertEqual(self.playback.repeat, False)
def test_random_off_by_default(self):
self.assertEqual(self.playback.random, False)
def test_consume_off_by_default(self):
self.assertEqual(self.playback.consume, False)
@populate_tracklist
def test_random_until_end_of_playlist(self):
self.playback.random = True
self.playback.play()
for _ in self.tracks[1:]:
self.playback.next()
self.assertEqual(self.playback.tl_track_at_next, None)
@populate_tracklist
def test_random_until_end_of_playlist_and_play_from_start(self):
self.playback.repeat = True
for _ in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.tl_track_at_next, None)
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
@populate_tracklist
def test_random_until_end_of_playlist_with_repeat(self):
self.playback.repeat = True
self.playback.random = True
self.playback.play()
for _ in self.tracks:
self.playback.next()
self.assertNotEqual(self.playback.tl_track_at_next, None)
@populate_tracklist
def test_played_track_during_random_not_played_again(self):
self.playback.random = True
self.playback.play()
played = []
for _ in self.tracks:
self.assertNotIn(self.playback.current_track, played)
played.append(self.playback.current_track)
self.playback.next()
@populate_tracklist
def test_playing_track_that_isnt_in_playlist(self):
test = lambda: self.playback.play((17, Track()))
self.assertRaises(AssertionError, test)

View File

@ -1,102 +0,0 @@
from __future__ import unicode_literals
import unittest
import pykka
from mopidy import audio, core
from mopidy.models import Playlist
class PlaylistsControllerTest(object):
config = {}
def setUp(self):
self.audio = audio.DummyAudio.start().proxy()
self.backend = self.backend_class.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core(backends=[self.backend])
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_create_returns_playlist_with_name_set(self):
playlist = self.core.playlists.create('test')
self.assertEqual(playlist.name, 'test')
def test_create_returns_playlist_with_uri_set(self):
playlist = self.core.playlists.create('test')
self.assert_(playlist.uri)
def test_create_adds_playlist_to_playlists_collection(self):
playlist = self.core.playlists.create('test')
self.assert_(self.core.playlists.playlists)
self.assertIn(playlist, self.core.playlists.playlists)
def test_playlists_empty_to_start_with(self):
self.assert_(not self.core.playlists.playlists)
def test_delete_non_existant_playlist(self):
self.core.playlists.delete('file:///unknown/playlist')
def test_delete_playlist_removes_it_from_the_collection(self):
playlist = self.core.playlists.create('test')
self.assertIn(playlist, self.core.playlists.playlists)
self.core.playlists.delete(playlist.uri)
self.assertNotIn(playlist, self.core.playlists.playlists)
def test_filter_without_criteria(self):
self.assertEqual(
self.core.playlists.playlists, self.core.playlists.filter())
def test_filter_with_wrong_criteria(self):
self.assertEqual([], self.core.playlists.filter(name='foo'))
def test_filter_with_right_criteria(self):
playlist = self.core.playlists.create('test')
playlists = self.core.playlists.filter(name='test')
self.assertEqual([playlist], playlists)
def test_filter_by_name_returns_single_match(self):
playlist = Playlist(name='b')
self.backend.playlists.playlists = [Playlist(name='a'), playlist]
self.assertEqual([playlist], self.core.playlists.filter(name='b'))
def test_filter_by_name_returns_multiple_matches(self):
playlist = Playlist(name='b')
self.backend.playlists.playlists = [
playlist, Playlist(name='a'), Playlist(name='b')]
playlists = self.core.playlists.filter(name='b')
self.assertIn(playlist, playlists)
self.assertEqual(2, len(playlists))
def test_filter_by_name_returns_no_matches(self):
self.backend.playlists.playlists = [
Playlist(name='a'), Playlist(name='b')]
self.assertEqual([], self.core.playlists.filter(name='c'))
def test_lookup_finds_playlist_by_uri(self):
original_playlist = self.core.playlists.create('test')
looked_up_playlist = self.core.playlists.lookup(original_playlist.uri)
self.assertEqual(original_playlist, looked_up_playlist)
@unittest.SkipTest
def test_refresh(self):
pass
def test_save_replaces_existing_playlist_with_updated_playlist(self):
playlist1 = self.core.playlists.create('test1')
self.assertIn(playlist1, self.core.playlists.playlists)
playlist2 = playlist1.copy(name='test2')
playlist2 = self.core.playlists.save(playlist2)
self.assertNotIn(playlist1, self.core.playlists.playlists)
self.assertIn(playlist2, self.core.playlists.playlists)
@unittest.SkipTest
def test_playlist_with_unknown_track(self):
pass

View File

@ -1,295 +0,0 @@
from __future__ import unicode_literals
import random
import pykka
from mopidy import audio, core
from mopidy.core import PlaybackState
from mopidy.models import TlTrack, Playlist, Track
from tests.backends.base import populate_tracklist
class TracklistControllerTest(object):
tracks = []
config = {}
def setUp(self):
self.audio = audio.DummyAudio.start().proxy()
self.backend = self.backend_class.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core(audio=self.audio, backends=[self.backend])
self.controller = self.core.tracklist
self.playback = self.core.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_length(self):
self.assertEqual(0, len(self.controller.tl_tracks))
self.assertEqual(0, self.controller.length)
self.controller.add(self.tracks)
self.assertEqual(3, len(self.controller.tl_tracks))
self.assertEqual(3, self.controller.length)
def test_add(self):
for track in self.tracks:
tl_tracks = self.controller.add([track])
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1])
self.assertEqual(track, tl_tracks[0].track)
def test_add_at_position(self):
for track in self.tracks[:-1]:
tl_tracks = self.controller.add([track], 0)
self.assertEqual(track, self.controller.tracks[0])
self.assertEqual(tl_tracks[0], self.controller.tl_tracks[0])
self.assertEqual(track, tl_tracks[0].track)
@populate_tracklist
def test_add_at_position_outside_of_playlist(self):
for track in self.tracks:
tl_tracks = self.controller.add([track], len(self.tracks) + 2)
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1])
self.assertEqual(track, tl_tracks[0].track)
@populate_tracklist
def test_filter_by_tlid(self):
tl_track = self.controller.tl_tracks[1]
self.assertEqual(
[tl_track], self.controller.filter(tlid=tl_track.tlid))
@populate_tracklist
def test_filter_by_uri(self):
tl_track = self.controller.tl_tracks[1]
self.assertEqual(
[tl_track], self.controller.filter(uri=tl_track.track.uri))
@populate_tracklist
def test_filter_by_uri_returns_nothing_for_invalid_uri(self):
self.assertEqual([], self.controller.filter(uri='foobar'))
def test_filter_by_uri_returns_single_match(self):
track = Track(uri='a')
self.controller.add([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.filter(uri='a')[0].track)
def test_filter_by_uri_returns_multiple_matches(self):
track = Track(uri='a')
self.controller.add([Track(uri='z'), track, track])
tl_tracks = self.controller.filter(uri='a')
self.assertEqual(track, tl_tracks[0].track)
self.assertEqual(track, tl_tracks[1].track)
def test_filter_by_uri_returns_nothing_if_no_match(self):
self.controller.playlist = Playlist(
tracks=[Track(uri='z'), Track(uri='y')])
self.assertEqual([], self.controller.filter(uri='a'))
def test_filter_by_multiple_criteria_returns_elements_matching_all(self):
track1 = Track(uri='a', name='x')
track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y')
self.controller.add([track1, track2, track3])
self.assertEqual(
track1, self.controller.filter(uri='a', name='x')[0].track)
self.assertEqual(
track2, self.controller.filter(uri='b', name='x')[0].track)
self.assertEqual(
track3, self.controller.filter(uri='b', name='y')[0].track)
def test_filter_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track()
track2 = Track(uri='b')
track3 = Track()
self.controller.add([track1, track2, track3])
self.assertEqual(track2, self.controller.filter(uri='b')[0].track)
@populate_tracklist
def test_clear(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
def test_clear_empty_playlist(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
@populate_tracklist
def test_clear_when_playing(self):
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.controller.clear()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_add_appends_to_the_tracklist(self):
self.controller.add([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
self.controller.add([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
def test_add_does_not_reset_version(self):
version = self.controller.version
self.controller.add([])
self.assertEqual(self.controller.version, version)
@populate_tracklist
def test_add_preserves_playing_state(self):
self.playback.play()
track = self.playback.current_track
self.controller.add(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
@populate_tracklist
def test_add_preserves_stopped_state(self):
self.controller.add(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_tracklist
def test_add_returns_the_tl_tracks_that_was_added(self):
tl_tracks = self.controller.add(self.controller.tracks[1:2])
self.assertEqual(tl_tracks[0].track, self.controller.tracks[1])
def test_index_returns_index_of_track(self):
tl_tracks = self.controller.add(self.tracks)
self.assertEquals(0, self.controller.index(tl_tracks[0]))
self.assertEquals(1, self.controller.index(tl_tracks[1]))
self.assertEquals(2, self.controller.index(tl_tracks[2]))
def test_index_raises_value_error_if_item_not_found(self):
test = lambda: self.controller.index(TlTrack(0, Track()))
self.assertRaises(ValueError, test)
@populate_tracklist
def test_move_single(self):
self.controller.move(0, 0, 2)
tracks = self.controller.tracks
self.assertEqual(tracks[2], self.tracks[0])
@populate_tracklist
def test_move_group(self):
self.controller.move(0, 2, 1)
tracks = self.controller.tracks
self.assertEqual(tracks[1], self.tracks[0])
self.assertEqual(tracks[2], self.tracks[1])
@populate_tracklist
def test_moving_track_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 0, tracks + 5)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_move_group_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 2, tracks + 5)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_move_group_out_of_range(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(tracks + 2, tracks + 3, 0)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_move_group_invalid_group(self):
test = lambda: self.controller.move(2, 1, 0)
self.assertRaises(AssertionError, test)
def test_tracks_attribute_is_immutable(self):
tracks1 = self.controller.tracks
tracks2 = self.controller.tracks
self.assertNotEqual(id(tracks1), id(tracks2))
@populate_tracklist
def test_remove(self):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=track1.uri)
self.assertLess(version, self.controller.version)
self.assertNotIn(track1, self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
@populate_tracklist
def test_removing_track_that_does_not_exist_does_nothing(self):
self.controller.remove(uri='/nonexistant')
def test_removing_from_empty_playlist_does_nothing(self):
self.controller.remove(uri='/nonexistant')
@populate_tracklist
def test_shuffle(self):
random.seed(1)
self.controller.shuffle()
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_tracklist
def test_shuffle_subset(self):
random.seed(1)
self.controller.shuffle(1, 3)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_tracklist
def test_shuffle_invalid_subset(self):
test = lambda: self.controller.shuffle(3, 1)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_shuffle_superset(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.shuffle(1, tracks + 5)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_shuffle_open_subset(self):
random.seed(1)
self.controller.shuffle(1)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_tracklist
def test_slice_returns_a_subset_of_tracks(self):
track_slice = self.controller.slice(1, 3)
self.assertEqual(2, len(track_slice))
self.assertEqual(self.tracks[1], track_slice[0].track)
self.assertEqual(self.tracks[2], track_slice[1].track)
@populate_tracklist
def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self):
self.assertEqual(0, len(self.controller.slice(7, 8)))
self.assertEqual(0, len(self.controller.slice(-1, 1)))
def test_version_does_not_change_when_adding_nothing(self):
version = self.controller.version
self.controller.add([])
self.assertEquals(version, self.controller.version)
def test_version_increases_when_adding_something(self):
version = self.controller.version
self.controller.add([Track()])
self.assertLess(version, self.controller.version)

View File

@ -1,4 +1,15 @@
from __future__ import unicode_literals
generate_song = lambda i: 'local:track:song%s.wav' % i
def generate_song(i):
return 'local:track:song%s.wav' % i
def populate_tracklist(func):
def wrapper(self):
self.tl_tracks = self.core.tracklist.add(self.tracks)
return func(self)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper

View File

@ -2,14 +2,18 @@ from __future__ import unicode_literals
import unittest
import mock
import pykka
from mopidy import core, audio
from mopidy.backends import listener
from mopidy.backends.local import actor
from tests import path_to_data_dir
from tests.backends.base import events
class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase):
backend_class = actor.LocalBackend
@mock.patch.object(listener.BackendListener, 'send')
class LocalBackendEventsTest(unittest.TestCase):
config = {
'local': {
'media_dir': path_to_data_dir(''),
@ -17,3 +21,17 @@ class LocalBackendEventsTest(events.BackendEventsTest, unittest.TestCase):
'tag_cache_file': path_to_data_dir('empty_tag_cache'),
}
}
def setUp(self):
self.audio = audio.DummyAudio.start().proxy()
self.backend = actor.LocalBackend.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_playlists_refresh_sends_playlists_loaded_event(self, send):
send.reset_mock()
self.core.playlists.refresh().get()
self.assertEqual(send.call_args[0][0], 'playlists_loaded')

View File

@ -1,15 +1,48 @@
from __future__ import unicode_literals
import tempfile
import unittest
import pykka
from mopidy import core
from mopidy.backends.local import actor
from mopidy.models import Track, Album, Artist
from tests import path_to_data_dir
from tests.backends.base.library import LibraryControllerTest
class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
backend_class = actor.LocalBackend
# TODO: update tests to only use backend, not core. we need a seperate
# core test that does this integration test.
class LocalLibraryProviderTest(unittest.TestCase):
artists = [
Artist(name='artist1'),
Artist(name='artist2'),
Artist(name='artist3'),
Artist(name='artist4'),
]
albums = [
Album(name='album1', artists=[artists[0]]),
Album(name='album2', artists=[artists[1]]),
Album(name='album3', artists=[artists[2]]),
]
tracks = [
Track(
uri='local:track:path1', name='track1',
artists=[artists[0]], album=albums[0],
date='2001-02-03', length=4000, track_no=1),
Track(
uri='local:track:path2', name='track2',
artists=[artists[1]], album=albums[1],
date='2002', length=4000, track_no=2),
Track(
uri='local:track:path3', name='track3',
artists=[artists[3]], album=albums[2],
date='2003', length=4000, track_no=3),
]
config = {
'local': {
'media_dir': path_to_data_dir(''),
@ -17,3 +50,319 @@ class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase):
'tag_cache_file': path_to_data_dir('library_tag_cache'),
}
}
def setUp(self):
self.backend = actor.LocalBackend.start(
config=self.config, audio=None).proxy()
self.core = core.Core(backends=[self.backend])
self.library = self.core.library
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_refresh(self):
self.library.refresh()
@unittest.SkipTest
def test_refresh_uri(self):
pass
def test_refresh_missing_uri(self):
# Verifies that https://github.com/mopidy/mopidy/issues/500
# has been fixed.
tag_cache = tempfile.NamedTemporaryFile()
with open(self.config['local']['tag_cache_file']) as fh:
tag_cache.write(fh.read())
tag_cache.flush()
config = {'local': self.config['local'].copy()}
config['local']['tag_cache_file'] = tag_cache.name
backend = actor.LocalBackend(config=config, audio=None)
# Sanity check that value is in tag cache
result = backend.library.lookup(self.tracks[0].uri)
self.assertEqual(result, self.tracks[0:1])
# Clear tag cache and refresh
tag_cache.seek(0)
tag_cache.truncate()
backend.library.refresh()
# Now it should be gone.
result = backend.library.lookup(self.tracks[0].uri)
self.assertEqual(result, [])
def test_lookup(self):
tracks = self.library.lookup(self.tracks[0].uri)
self.assertEqual(tracks, self.tracks[0:1])
def test_lookup_unknown_track(self):
tracks = self.library.lookup('fake uri')
self.assertEqual(tracks, [])
def test_find_exact_no_hits(self):
result = self.library.find_exact(track=['unknown track'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(artist=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(album=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(date=['1990'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(track_no=[9])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(uri=['fake uri'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(any=['unknown any'])
self.assertEqual(list(result[0].tracks), [])
def test_find_exact_uri(self):
track_1_uri = 'local:track:path1'
result = self.library.find_exact(uri=track_1_uri)
self.assertEqual(list(result[0].tracks), self.tracks[:1])
track_2_uri = 'local:track:path2'
result = self.library.find_exact(uri=track_2_uri)
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_track(self):
result = self.library.find_exact(track=['track1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(track=['track2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_artist(self):
result = self.library.find_exact(artist=['artist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(artist=['artist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_album(self):
result = self.library.find_exact(album=['album1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(album=['album2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_albumartist(self):
# Artist is both track artist and album artist
result = self.library.find_exact(albumartist=['artist1'])
self.assertEqual(list(result[0].tracks), [self.tracks[0]])
# Artist is both track and album artist
result = self.library.find_exact(albumartist=['artist2'])
self.assertEqual(list(result[0].tracks), [self.tracks[1]])
# Artist is just album artist
result = self.library.find_exact(albumartist=['artist3'])
self.assertEqual(list(result[0].tracks), [self.tracks[2]])
def test_find_exact_track_no(self):
result = self.library.find_exact(track_no=[1])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(track_no=[2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_date(self):
result = self.library.find_exact(date=['2001'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.find_exact(date=['2001-02-03'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(date=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_find_exact_any(self):
# Matches on track artist
result = self.library.find_exact(any=['artist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(any=['artist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
# Matches on track
result = self.library.find_exact(any=['track1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.find_exact(any=['track2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
# Matches on track album
result = self.library.find_exact(any=['album1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
# Matches on track album artists
result = self.library.find_exact(any=['artist3'])
self.assertEqual(list(result[0].tracks), self.tracks[2:3])
# Matches on track year
result = self.library.find_exact(any=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
# Matches on URI
result = self.library.find_exact(any=['local:track:path1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_find_exact_wrong_type(self):
test = lambda: self.library.find_exact(wrong=['test'])
self.assertRaises(LookupError, test)
def test_find_exact_with_empty_query(self):
test = lambda: self.library.find_exact(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(track_no=[])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(date=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.find_exact(any=[''])
self.assertRaises(LookupError, test)
def test_search_no_hits(self):
result = self.library.search(track=['unknown track'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(artist=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(album=['unknown artist'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(track_no=[9])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(date=['unknown date'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(uri=['unknown uri'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(any=['unknown anything'])
self.assertEqual(list(result[0].tracks), [])
def test_search_uri(self):
result = self.library.search(uri=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(uri=['TH2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track(self):
result = self.library.search(track=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(track=['Rack2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_artist(self):
result = self.library.search(artist=['Tist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(artist=['Tist2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_albumartist(self):
# Artist is both track artist and album artist
result = self.library.search(albumartist=['Tist1'])
self.assertEqual(list(result[0].tracks), [self.tracks[0]])
# Artist is both track artist and album artist
result = self.library.search(albumartist=['Tist2'])
self.assertEqual(list(result[0].tracks), [self.tracks[1]])
# Artist is just album artist
result = self.library.search(albumartist=['Tist3'])
self.assertEqual(list(result[0].tracks), [self.tracks[2]])
def test_search_album(self):
result = self.library.search(album=['Bum1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(album=['Bum2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_date(self):
result = self.library.search(date=['2001'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-03'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(date=['2001-02-04'])
self.assertEqual(list(result[0].tracks), [])
result = self.library.search(date=['2002'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_track_no(self):
result = self.library.search(track_no=[1])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(track_no=[2])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
def test_search_any(self):
# Matches on track artist
result = self.library.search(any=['Tist1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
# Matches on track
result = self.library.search(any=['Rack1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
result = self.library.search(any=['Rack2'])
self.assertEqual(list(result[0].tracks), self.tracks[1:2])
# Matches on track album
result = self.library.search(any=['Bum1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
# Matches on track album artists
result = self.library.search(any=['Tist3'])
self.assertEqual(list(result[0].tracks), self.tracks[2:3])
# Matches on URI
result = self.library.search(any=['TH1'])
self.assertEqual(list(result[0].tracks), self.tracks[:1])
def test_search_wrong_type(self):
test = lambda: self.library.search(wrong=['test'])
self.assertRaises(LookupError, test)
def test_search_with_empty_query(self):
test = lambda: self.library.search(artist=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(track=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(album=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(date=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(uri=[''])
self.assertRaises(LookupError, test)
test = lambda: self.library.search(any=[''])
self.assertRaises(LookupError, test)

File diff suppressed because it is too large Load Diff

View File

@ -5,18 +5,17 @@ import shutil
import tempfile
import unittest
import pykka
from mopidy import audio, core
from mopidy.backends.local import actor
from mopidy.models import Track
from mopidy.models import Playlist, Track
from tests import path_to_data_dir
from tests.backends.base.playlists import (
PlaylistsControllerTest)
from tests.backends.local import generate_song
class LocalPlaylistsControllerTest(
PlaylistsControllerTest, unittest.TestCase):
class LocalPlaylistsProviderTest(unittest.TestCase):
backend_class = actor.LocalBackend
config = {
'local': {
@ -29,10 +28,13 @@ class LocalPlaylistsControllerTest(
self.config['local']['playlists_dir'] = tempfile.mkdtemp()
self.playlists_dir = self.config['local']['playlists_dir']
super(LocalPlaylistsControllerTest, self).setUp()
self.audio = audio.DummyAudio.start().proxy()
self.backend = actor.LocalBackend.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core(backends=[self.backend])
def tearDown(self):
super(LocalPlaylistsControllerTest, self).tearDown()
pykka.ActorRegistry.stop_all()
if os.path.exists(self.playlists_dir):
shutil.rmtree(self.playlists_dir)
@ -121,3 +123,96 @@ class LocalPlaylistsControllerTest(
@unittest.SkipTest
def test_playlist_dir_is_created(self):
pass
def test_create_returns_playlist_with_name_set(self):
playlist = self.core.playlists.create('test')
self.assertEqual(playlist.name, 'test')
def test_create_returns_playlist_with_uri_set(self):
playlist = self.core.playlists.create('test')
self.assert_(playlist.uri)
def test_create_adds_playlist_to_playlists_collection(self):
playlist = self.core.playlists.create('test')
self.assert_(self.core.playlists.playlists)
self.assertIn(playlist, self.core.playlists.playlists)
def test_playlists_empty_to_start_with(self):
self.assert_(not self.core.playlists.playlists)
def test_delete_non_existant_playlist(self):
self.core.playlists.delete('file:///unknown/playlist')
def test_delete_playlist_removes_it_from_the_collection(self):
playlist = self.core.playlists.create('test')
self.assertIn(playlist, self.core.playlists.playlists)
self.core.playlists.delete(playlist.uri)
self.assertNotIn(playlist, self.core.playlists.playlists)
def test_filter_without_criteria(self):
self.assertEqual(
self.core.playlists.playlists, self.core.playlists.filter())
def test_filter_with_wrong_criteria(self):
self.assertEqual([], self.core.playlists.filter(name='foo'))
def test_filter_with_right_criteria(self):
playlist = self.core.playlists.create('test')
playlists = self.core.playlists.filter(name='test')
self.assertEqual([playlist], playlists)
def test_filter_by_name_returns_single_match(self):
playlist = Playlist(name='b')
self.backend.playlists.playlists = [Playlist(name='a'), playlist]
self.assertEqual([playlist], self.core.playlists.filter(name='b'))
def test_filter_by_name_returns_multiple_matches(self):
playlist = Playlist(name='b')
self.backend.playlists.playlists = [
playlist, Playlist(name='a'), Playlist(name='b')]
playlists = self.core.playlists.filter(name='b')
self.assertIn(playlist, playlists)
self.assertEqual(2, len(playlists))
def test_filter_by_name_returns_no_matches(self):
self.backend.playlists.playlists = [
Playlist(name='a'), Playlist(name='b')]
self.assertEqual([], self.core.playlists.filter(name='c'))
def test_lookup_finds_playlist_by_uri(self):
original_playlist = self.core.playlists.create('test')
looked_up_playlist = self.core.playlists.lookup(original_playlist.uri)
self.assertEqual(original_playlist, looked_up_playlist)
@unittest.SkipTest
def test_refresh(self):
pass
def test_save_replaces_existing_playlist_with_updated_playlist(self):
playlist1 = self.core.playlists.create('test1')
self.assertIn(playlist1, self.core.playlists.playlists)
playlist2 = playlist1.copy(name='test2')
playlist2 = self.core.playlists.save(playlist2)
self.assertNotIn(playlist1, self.core.playlists.playlists)
self.assertIn(playlist2, self.core.playlists.playlists)
def test_playlist_with_unknown_track(self):
track = Track(uri='file:///dev/null')
playlist = self.core.playlists.create('test')
playlist = playlist.copy(tracks=[track])
playlist = self.core.playlists.save(playlist)
backend = self.backend_class(config=self.config, audio=self.audio)
self.assert_(backend.playlists.playlists)
self.assertEqual(
'local:playlist:test', backend.playlists.playlists[0].uri)
self.assertEqual(
playlist.name, backend.playlists.playlists[0].name)
self.assertEqual(
track.uri, backend.playlists.playlists[0].tracks[0].uri)

View File

@ -1,17 +1,20 @@
from __future__ import unicode_literals
import random
import unittest
import pykka
from mopidy import audio, core
from mopidy.backends.local import actor
from mopidy.models import Track
from mopidy.core import PlaybackState
from mopidy.models import Playlist, TlTrack, Track
from tests import path_to_data_dir
from tests.backends.base.tracklist import TracklistControllerTest
from tests.backends.local import generate_song
from tests.backends.local import generate_song, populate_tracklist
class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase):
backend_class = actor.LocalBackend
class LocalTracklistProviderTest(unittest.TestCase):
config = {
'local': {
'media_dir': path_to_data_dir(''),
@ -21,3 +24,282 @@ class LocalTracklistControllerTest(TracklistControllerTest, unittest.TestCase):
}
tracks = [
Track(uri=generate_song(i), length=4464) for i in range(1, 4)]
def setUp(self):
self.audio = audio.DummyAudio.start().proxy()
self.backend = actor.LocalBackend.start(
config=self.config, audio=self.audio).proxy()
self.core = core.Core(audio=self.audio, backends=[self.backend])
self.controller = self.core.tracklist
self.playback = self.core.playback
assert len(self.tracks) == 3, 'Need three tracks to run tests.'
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_length(self):
self.assertEqual(0, len(self.controller.tl_tracks))
self.assertEqual(0, self.controller.length)
self.controller.add(self.tracks)
self.assertEqual(3, len(self.controller.tl_tracks))
self.assertEqual(3, self.controller.length)
def test_add(self):
for track in self.tracks:
tl_tracks = self.controller.add([track])
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1])
self.assertEqual(track, tl_tracks[0].track)
def test_add_at_position(self):
for track in self.tracks[:-1]:
tl_tracks = self.controller.add([track], 0)
self.assertEqual(track, self.controller.tracks[0])
self.assertEqual(tl_tracks[0], self.controller.tl_tracks[0])
self.assertEqual(track, tl_tracks[0].track)
@populate_tracklist
def test_add_at_position_outside_of_playlist(self):
for track in self.tracks:
tl_tracks = self.controller.add([track], len(self.tracks) + 2)
self.assertEqual(track, self.controller.tracks[-1])
self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1])
self.assertEqual(track, tl_tracks[0].track)
@populate_tracklist
def test_filter_by_tlid(self):
tl_track = self.controller.tl_tracks[1]
self.assertEqual(
[tl_track], self.controller.filter(tlid=tl_track.tlid))
@populate_tracklist
def test_filter_by_uri(self):
tl_track = self.controller.tl_tracks[1]
self.assertEqual(
[tl_track], self.controller.filter(uri=tl_track.track.uri))
@populate_tracklist
def test_filter_by_uri_returns_nothing_for_invalid_uri(self):
self.assertEqual([], self.controller.filter(uri='foobar'))
def test_filter_by_uri_returns_single_match(self):
track = Track(uri='a')
self.controller.add([Track(uri='z'), track, Track(uri='y')])
self.assertEqual(track, self.controller.filter(uri='a')[0].track)
def test_filter_by_uri_returns_multiple_matches(self):
track = Track(uri='a')
self.controller.add([Track(uri='z'), track, track])
tl_tracks = self.controller.filter(uri='a')
self.assertEqual(track, tl_tracks[0].track)
self.assertEqual(track, tl_tracks[1].track)
def test_filter_by_uri_returns_nothing_if_no_match(self):
self.controller.playlist = Playlist(
tracks=[Track(uri='z'), Track(uri='y')])
self.assertEqual([], self.controller.filter(uri='a'))
def test_filter_by_multiple_criteria_returns_elements_matching_all(self):
track1 = Track(uri='a', name='x')
track2 = Track(uri='b', name='x')
track3 = Track(uri='b', name='y')
self.controller.add([track1, track2, track3])
self.assertEqual(
track1, self.controller.filter(uri='a', name='x')[0].track)
self.assertEqual(
track2, self.controller.filter(uri='b', name='x')[0].track)
self.assertEqual(
track3, self.controller.filter(uri='b', name='y')[0].track)
def test_filter_by_criteria_that_is_not_present_in_all_elements(self):
track1 = Track()
track2 = Track(uri='b')
track3 = Track()
self.controller.add([track1, track2, track3])
self.assertEqual(track2, self.controller.filter(uri='b')[0].track)
@populate_tracklist
def test_clear(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
def test_clear_empty_playlist(self):
self.controller.clear()
self.assertEqual(len(self.controller.tracks), 0)
@populate_tracklist
def test_clear_when_playing(self):
self.playback.play()
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.controller.clear()
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
def test_add_appends_to_the_tracklist(self):
self.controller.add([Track(uri='a'), Track(uri='b')])
self.assertEqual(len(self.controller.tracks), 2)
self.controller.add([Track(uri='c'), Track(uri='d')])
self.assertEqual(len(self.controller.tracks), 4)
self.assertEqual(self.controller.tracks[0].uri, 'a')
self.assertEqual(self.controller.tracks[1].uri, 'b')
self.assertEqual(self.controller.tracks[2].uri, 'c')
self.assertEqual(self.controller.tracks[3].uri, 'd')
def test_add_does_not_reset_version(self):
version = self.controller.version
self.controller.add([])
self.assertEqual(self.controller.version, version)
@populate_tracklist
def test_add_preserves_playing_state(self):
self.playback.play()
track = self.playback.current_track
self.controller.add(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, PlaybackState.PLAYING)
self.assertEqual(self.playback.current_track, track)
@populate_tracklist
def test_add_preserves_stopped_state(self):
self.controller.add(self.controller.tracks[1:2])
self.assertEqual(self.playback.state, PlaybackState.STOPPED)
self.assertEqual(self.playback.current_track, None)
@populate_tracklist
def test_add_returns_the_tl_tracks_that_was_added(self):
tl_tracks = self.controller.add(self.controller.tracks[1:2])
self.assertEqual(tl_tracks[0].track, self.controller.tracks[1])
def test_index_returns_index_of_track(self):
tl_tracks = self.controller.add(self.tracks)
self.assertEqual(0, self.controller.index(tl_tracks[0]))
self.assertEqual(1, self.controller.index(tl_tracks[1]))
self.assertEqual(2, self.controller.index(tl_tracks[2]))
def test_index_returns_none_if_item_not_found(self):
tl_track = TlTrack(0, Track())
self.assertEqual(self.controller.index(tl_track), None)
@populate_tracklist
def test_move_single(self):
self.controller.move(0, 0, 2)
tracks = self.controller.tracks
self.assertEqual(tracks[2], self.tracks[0])
@populate_tracklist
def test_move_group(self):
self.controller.move(0, 2, 1)
tracks = self.controller.tracks
self.assertEqual(tracks[1], self.tracks[0])
self.assertEqual(tracks[2], self.tracks[1])
@populate_tracklist
def test_moving_track_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 0, tracks + 5)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_move_group_outside_of_playlist(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(0, 2, tracks + 5)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_move_group_out_of_range(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.move(tracks + 2, tracks + 3, 0)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_move_group_invalid_group(self):
test = lambda: self.controller.move(2, 1, 0)
self.assertRaises(AssertionError, test)
def test_tracks_attribute_is_immutable(self):
tracks1 = self.controller.tracks
tracks2 = self.controller.tracks
self.assertNotEqual(id(tracks1), id(tracks2))
@populate_tracklist
def test_remove(self):
track1 = self.controller.tracks[1]
track2 = self.controller.tracks[2]
version = self.controller.version
self.controller.remove(uri=track1.uri)
self.assertLess(version, self.controller.version)
self.assertNotIn(track1, self.controller.tracks)
self.assertEqual(track2, self.controller.tracks[1])
@populate_tracklist
def test_removing_track_that_does_not_exist_does_nothing(self):
self.controller.remove(uri='/nonexistant')
def test_removing_from_empty_playlist_does_nothing(self):
self.controller.remove(uri='/nonexistant')
@populate_tracklist
def test_shuffle(self):
random.seed(1)
self.controller.shuffle()
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_tracklist
def test_shuffle_subset(self):
random.seed(1)
self.controller.shuffle(1, 3)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_tracklist
def test_shuffle_invalid_subset(self):
test = lambda: self.controller.shuffle(3, 1)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_shuffle_superset(self):
tracks = len(self.controller.tracks)
test = lambda: self.controller.shuffle(1, tracks + 5)
self.assertRaises(AssertionError, test)
@populate_tracklist
def test_shuffle_open_subset(self):
random.seed(1)
self.controller.shuffle(1)
shuffled_tracks = self.controller.tracks
self.assertNotEqual(self.tracks, shuffled_tracks)
self.assertEqual(self.tracks[0], shuffled_tracks[0])
self.assertEqual(set(self.tracks), set(shuffled_tracks))
@populate_tracklist
def test_slice_returns_a_subset_of_tracks(self):
track_slice = self.controller.slice(1, 3)
self.assertEqual(2, len(track_slice))
self.assertEqual(self.tracks[1], track_slice[0].track)
self.assertEqual(self.tracks[2], track_slice[1].track)
@populate_tracklist
def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self):
self.assertEqual(0, len(self.controller.slice(7, 8)))
self.assertEqual(0, len(self.controller.slice(-1, 1)))
def test_version_does_not_change_when_adding_nothing(self):
version = self.controller.version
self.controller.add([])
self.assertEquals(version, self.controller.version)
def test_version_increases_when_adding_something(self):
version = self.controller.version
self.controller.add([Track()])
self.assertLess(version, self.controller.version)

View File

@ -106,3 +106,162 @@ class ValidateTest(unittest.TestCase):
self.assertEqual({'foo': {'bar': 'bad'}}, errors)
# TODO: add more tests
INPUT_CONFIG = """# comments before first section should work
[section] anything goes ; after the [] block it seems.
; this is a valid comment
this-should-equal-baz = baz ; as this is a comment
this-should-equal-everything = baz # as this is not a comment
# this is also a comment ; and the next line should be a blank comment.
;
# foo # = should all be treated as a comment."""
PROCESSED_CONFIG = """[__COMMENTS__]
__HASH0__ = comments before first section should work
__BLANK1__ =
[section]
__SECTION2__ = anything goes
__INLINE3__ = after the [] block it seems.
__SEMICOLON4__ = this is a valid comment
this-should-equal-baz = baz
__INLINE5__ = as this is a comment
this-should-equal-everything = baz # as this is not a comment
__BLANK6__ =
__HASH7__ = this is also a comment
__INLINE8__ = and the next line should be a blank comment.
__SEMICOLON9__ =
__HASH10__ = foo # = should all be treated as a comment."""
class PreProcessorTest(unittest.TestCase):
maxDiff = None # Show entire diff.
def test_empty_config(self):
result = config._preprocess('')
self.assertEqual(result, '[__COMMENTS__]')
def test_plain_section(self):
result = config._preprocess('[section]\nfoo = bar')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar')
def test_initial_comments(self):
result = config._preprocess('; foobar')
self.assertEqual(result, '[__COMMENTS__]\n'
'__SEMICOLON0__ = foobar')
result = config._preprocess('# foobar')
self.assertEqual(result, '[__COMMENTS__]\n'
'__HASH0__ = foobar')
result = config._preprocess('; foo\n# bar')
self.assertEqual(result, '[__COMMENTS__]\n'
'__SEMICOLON0__ = foo\n'
'__HASH1__ = bar')
def test_initial_comment_inline_handling(self):
result = config._preprocess('; foo ; bar ; baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'__SEMICOLON0__ = foo\n'
'__INLINE1__ = bar\n'
'__INLINE2__ = baz')
def test_inline_semicolon_comment(self):
result = config._preprocess('[section]\nfoo = bar ; baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar\n'
'__INLINE0__ = baz')
def test_no_inline_hash_comment(self):
result = config._preprocess('[section]\nfoo = bar # baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar # baz')
def test_section_extra_text(self):
result = config._preprocess('[section] foobar')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'__SECTION0__ = foobar')
def test_section_extra_text_inline_semicolon(self):
result = config._preprocess('[section] foobar ; baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'__SECTION0__ = foobar\n'
'__INLINE1__ = baz')
def test_conversion(self):
"""Tests all of the above cases at once."""
result = config._preprocess(INPUT_CONFIG)
self.assertEqual(result, PROCESSED_CONFIG)
class PostProcessorTest(unittest.TestCase):
maxDiff = None # Show entire diff.
def test_empty_config(self):
result = config._postprocess('[__COMMENTS__]')
self.assertEqual(result, '')
def test_plain_section(self):
result = config._postprocess('[__COMMENTS__]\n'
'[section]\n'
'foo = bar')
self.assertEqual(result, '[section]\nfoo = bar')
def test_initial_comments(self):
result = config._postprocess('[__COMMENTS__]\n'
'__SEMICOLON0__ = foobar')
self.assertEqual(result, '; foobar')
result = config._postprocess('[__COMMENTS__]\n'
'__HASH0__ = foobar')
self.assertEqual(result, '# foobar')
result = config._postprocess('[__COMMENTS__]\n'
'__SEMICOLON0__ = foo\n'
'__HASH1__ = bar')
self.assertEqual(result, '; foo\n# bar')
def test_initial_comment_inline_handling(self):
result = config._postprocess('[__COMMENTS__]\n'
'__SEMICOLON0__ = foo\n'
'__INLINE1__ = bar\n'
'__INLINE2__ = baz')
self.assertEqual(result, '; foo ; bar ; baz')
def test_inline_semicolon_comment(self):
result = config._postprocess('[__COMMENTS__]\n'
'[section]\n'
'foo = bar\n'
'__INLINE0__ = baz')
self.assertEqual(result, '[section]\nfoo = bar ; baz')
def test_no_inline_hash_comment(self):
result = config._preprocess('[section]\nfoo = bar # baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar # baz')
def test_section_extra_text(self):
result = config._postprocess('[__COMMENTS__]\n'
'[section]\n'
'__SECTION0__ = foobar')
self.assertEqual(result, '[section] foobar')
def test_section_extra_text_inline_semicolon(self):
result = config._postprocess('[__COMMENTS__]\n'
'[section]\n'
'__SECTION0__ = foobar\n'
'__INLINE1__ = baz')
self.assertEqual(result, '[section] foobar ; baz')
def test_conversion(self):
result = config._postprocess(PROCESSED_CONFIG)
self.assertEqual(result, INPUT_CONFIG)

View File

@ -49,7 +49,10 @@ class CoreListenerTest(unittest.TestCase):
self.listener.options_changed()
def test_listener_has_default_impl_for_volume_changed(self):
self.listener.volume_changed()
self.listener.volume_changed(70)
def test_listener_has_default_impl_for_mute_changed(self):
self.listener.mute_changed(True)
def test_listener_has_default_impl_for_seeked(self):
self.listener.seeked(0)

View File

@ -177,3 +177,10 @@ class CorePlaybackTest(unittest.TestCase):
self.assertEqual(result, 0)
self.assertFalse(self.playback1.get_time_position.called)
self.assertFalse(self.playback2.get_time_position.called)
def test_mute(self):
self.assertEqual(self.core.playback.mute, False)
self.core.playback.mute = True
self.assertEqual(self.core.playback.mute, True)

View File

@ -9,18 +9,23 @@ Artist: artist1
Title: track1
Album: album1
Date: 2001-02-03
Track: 1
Time: 4
key: key1
key: key2
file: /path2
Artist: artist2
Title: track2
Album: album2
Date: 2002
Track: 2
Time: 4
key: key3
file: /path3
Artist: artist3
Artist: artist4
AlbumArtist: artist3
Title: track3
Album: album3
Date: 2003
Track: 3
Time: 4
songList end

BIN
tests/data/song4.wav Normal file

Binary file not shown.

View File

@ -5,16 +5,48 @@ from tests.frontends.mpd import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self):
self.core.playback.mute = False
self.sendRequest('enableoutput "0"')
self.assertInResponse('ACK [0@0] {} Not implemented')
self.assertInResponse('OK')
self.assertEqual(self.core.playback.mute.get(), True)
def test_enableoutput_unknown_outputid(self):
self.sendRequest('enableoutput "7"')
self.assertInResponse('ACK [50@0] {enableoutput} No such audio output')
def test_disableoutput(self):
self.sendRequest('disableoutput "0"')
self.assertInResponse('ACK [0@0] {} Not implemented')
self.core.playback.mute = True
self.sendRequest('disableoutput "0"')
self.assertInResponse('OK')
self.assertEqual(self.core.playback.mute.get(), False)
def test_disableoutput_unknown_outputid(self):
self.sendRequest('disableoutput "7"')
self.assertInResponse(
'ACK [50@0] {disableoutput} No such audio output')
def test_outputs_when_unmuted(self):
self.core.playback.mute = False
def test_outputs(self):
self.sendRequest('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Default')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 0')
self.assertInResponse('OK')
def test_outputs_when_muted(self):
self.core.playback.mute = True
self.sendRequest('outputs')
self.assertInResponse('outputid: 0')
self.assertInResponse('outputname: Mute')
self.assertInResponse('outputenabled: 1')
self.assertInResponse('OK')

View File

@ -24,6 +24,28 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('playtime: 0')
self.assertInResponse('OK')
def test_count_correct_length(self):
# Count the lone track
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[
Track(uri='dummy:a', name="foo", date="2001", length=4000),
])
self.sendRequest('count "title" "foo"')
self.assertInResponse('songs: 1')
self.assertInResponse('playtime: 4')
self.assertInResponse('OK')
# Count multiple tracks
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[
Track(uri='dummy:b', date="2001", length=50000),
Track(uri='dummy:c', date="2001", length=600000),
])
self.sendRequest('count "date" "2001"')
self.assertInResponse('songs: 2')
self.assertInResponse('playtime: 650')
self.assertInResponse('OK')
def test_findadd(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(uri='dummy:a', name='A')])
@ -175,6 +197,26 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
self.assertInResponse('OK')
def test_find_albumartist_does_not_include_fake_artist_tracks(self):
self.backend.library.dummy_find_exact_result = SearchResult(
albums=[Album(uri='dummy:album:a', name='A', date='2001')],
artists=[Artist(uri='dummy:artist:b', name='B')],
tracks=[Track(uri='dummy:track:c', name='C')])
self.sendRequest('find "albumartist" "foo"')
self.assertNotInResponse('file: dummy:artist:b')
self.assertNotInResponse('Title: Artist: B')
self.assertInResponse('file: dummy:album:a')
self.assertInResponse('Title: Album: A')
self.assertInResponse('Date: 2001')
self.assertInResponse('file: dummy:track:c')
self.assertInResponse('Title: C')
self.assertInResponse('OK')
def test_find_artist_and_album_does_not_include_fake_tracks(self):
self.backend.library.dummy_find_exact_result = SearchResult(
albums=[Album(uri='dummy:album:a', name='A', date='2001')],
@ -211,6 +253,14 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
self.sendRequest('find artist "what"')
self.assertInResponse('OK')
def test_find_albumartist(self):
self.sendRequest('find "albumartist" "what"')
self.assertInResponse('OK')
def test_find_albumartist_without_quotes(self):
self.sendRequest('find albumartist "what"')
self.assertInResponse('OK')
def test_find_filename(self):
self.sendRequest('find "filename" "afilename"')
self.assertInResponse('OK')
@ -235,6 +285,18 @@ class MusicDatabaseFindTest(protocol.BaseTestCase):
self.sendRequest('find title "what"')
self.assertInResponse('OK')
def test_find_track_no(self):
self.sendRequest('find "track" "10"')
self.assertInResponse('OK')
def test_find_track_no_without_quotes(self):
self.sendRequest('find track "10"')
self.assertInResponse('OK')
def test_find_track_no_without_filter_value(self):
self.sendRequest('find "track" ""')
self.assertInResponse('OK')
def test_find_date(self):
self.sendRequest('find "date" "2002-01-01"')
self.assertInResponse('OK')
@ -366,6 +428,10 @@ class MusicDatabaseListTest(protocol.BaseTestCase):
self.sendRequest('list "album" "album" "analbum"')
self.assertInResponse('OK')
def test_list_album_by_albumartist(self):
self.sendRequest('list "album" "albumartist" "anartist"')
self.assertInResponse('OK')
def test_list_album_by_full_date(self):
self.sendRequest('list "album" "date" "2001-01-01"')
self.assertInResponse('OK')
@ -541,6 +607,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase):
self.sendRequest('search "artist" ""')
self.assertInResponse('OK')
def test_search_albumartist(self):
self.sendRequest('search "albumartist" "analbumartist"')
self.assertInResponse('OK')
def test_search_albumartist_without_quotes(self):
self.sendRequest('search albumartist "analbumartist"')
self.assertInResponse('OK')
def test_search_albumartist_without_filter_value(self):
self.sendRequest('search "albumartist" ""')
self.assertInResponse('OK')
def test_search_filename(self):
self.sendRequest('search "filename" "afilename"')
self.assertInResponse('OK')
@ -589,6 +667,18 @@ class MusicDatabaseSearchTest(protocol.BaseTestCase):
self.sendRequest('search "any" ""')
self.assertInResponse('OK')
def test_search_track_no(self):
self.sendRequest('search "track" "10"')
self.assertInResponse('OK')
def test_search_track_no_without_quotes(self):
self.sendRequest('search track "10"')
self.assertInResponse('OK')
def test_search_track_no_without_filter_value(self):
self.sendRequest('search "track" ""')
self.assertInResponse('OK')
def test_search_date(self):
self.sendRequest('search "date" "2002-01-01"')
self.assertInResponse('OK')

View File

@ -16,22 +16,22 @@ STOPPED = PlaybackState.STOPPED
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_consume_off(self):
self.sendRequest('consume "0"')
self.assertFalse(self.core.playback.consume.get())
self.assertFalse(self.core.tracklist.consume.get())
self.assertInResponse('OK')
def test_consume_off_without_quotes(self):
self.sendRequest('consume 0')
self.assertFalse(self.core.playback.consume.get())
self.assertFalse(self.core.tracklist.consume.get())
self.assertInResponse('OK')
def test_consume_on(self):
self.sendRequest('consume "1"')
self.assertTrue(self.core.playback.consume.get())
self.assertTrue(self.core.tracklist.consume.get())
self.assertInResponse('OK')
def test_consume_on_without_quotes(self):
self.sendRequest('consume 1')
self.assertTrue(self.core.playback.consume.get())
self.assertTrue(self.core.tracklist.consume.get())
self.assertInResponse('OK')
def test_crossfade(self):
@ -40,42 +40,42 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_random_off(self):
self.sendRequest('random "0"')
self.assertFalse(self.core.playback.random.get())
self.assertFalse(self.core.tracklist.random.get())
self.assertInResponse('OK')
def test_random_off_without_quotes(self):
self.sendRequest('random 0')
self.assertFalse(self.core.playback.random.get())
self.assertFalse(self.core.tracklist.random.get())
self.assertInResponse('OK')
def test_random_on(self):
self.sendRequest('random "1"')
self.assertTrue(self.core.playback.random.get())
self.assertTrue(self.core.tracklist.random.get())
self.assertInResponse('OK')
def test_random_on_without_quotes(self):
self.sendRequest('random 1')
self.assertTrue(self.core.playback.random.get())
self.assertTrue(self.core.tracklist.random.get())
self.assertInResponse('OK')
def test_repeat_off(self):
self.sendRequest('repeat "0"')
self.assertFalse(self.core.playback.repeat.get())
self.assertFalse(self.core.tracklist.repeat.get())
self.assertInResponse('OK')
def test_repeat_off_without_quotes(self):
self.sendRequest('repeat 0')
self.assertFalse(self.core.playback.repeat.get())
self.assertFalse(self.core.tracklist.repeat.get())
self.assertInResponse('OK')
def test_repeat_on(self):
self.sendRequest('repeat "1"')
self.assertTrue(self.core.playback.repeat.get())
self.assertTrue(self.core.tracklist.repeat.get())
self.assertInResponse('OK')
def test_repeat_on_without_quotes(self):
self.sendRequest('repeat 1')
self.assertTrue(self.core.playback.repeat.get())
self.assertTrue(self.core.tracklist.repeat.get())
self.assertInResponse('OK')
def test_setvol_below_min(self):
@ -115,22 +115,22 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_single_off(self):
self.sendRequest('single "0"')
self.assertFalse(self.core.playback.single.get())
self.assertFalse(self.core.tracklist.single.get())
self.assertInResponse('OK')
def test_single_off_without_quotes(self):
self.sendRequest('single 0')
self.assertFalse(self.core.playback.single.get())
self.assertFalse(self.core.tracklist.single.get())
self.assertInResponse('OK')
def test_single_on(self):
self.sendRequest('single "1"')
self.assertTrue(self.core.playback.single.get())
self.assertTrue(self.core.tracklist.single.get())
self.assertInResponse('OK')
def test_single_on_without_quotes(self):
self.sendRequest('single 1')
self.assertTrue(self.core.playback.single.get())
self.assertTrue(self.core.tracklist.single.get())
self.assertInResponse('OK')
def test_replay_gain_mode_off(self):

View File

@ -64,7 +64,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['repeat']), 0)
def test_status_method_contains_repeat_is_1(self):
self.core.playback.repeat = 1
self.core.tracklist.repeat = 1
result = dict(status.status(self.context))
self.assertIn('repeat', result)
self.assertEqual(int(result['repeat']), 1)
@ -75,7 +75,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['random']), 0)
def test_status_method_contains_random_is_1(self):
self.core.playback.random = 1
self.core.tracklist.random = 1
result = dict(status.status(self.context))
self.assertIn('random', result)
self.assertEqual(int(result['random']), 1)
@ -91,7 +91,7 @@ class StatusHandlerTest(unittest.TestCase):
self.assertEqual(int(result['consume']), 0)
def test_status_method_contains_consume_is_1(self):
self.core.playback.consume = 1
self.core.tracklist.consume = 1
result = dict(status.status(self.context))
self.assertIn('consume', result)
self.assertEqual(int(result['consume']), 1)

View File

@ -1 +0,0 @@
from __future__ import unicode_literals

View File

@ -1,92 +0,0 @@
from __future__ import unicode_literals
import mock
import unittest
try:
import dbus
except ImportError:
dbus = False
from mopidy.models import Playlist, TlTrack
if dbus:
from mopidy.frontends.mpris import actor, objects
@unittest.skipUnless(dbus, 'dbus not found')
class BackendEventsTest(unittest.TestCase):
def setUp(self):
# As a plain class, not an actor:
self.mpris_frontend = actor.MprisFrontend(config=None, core=None)
self.mpris_object = mock.Mock(spec=objects.MprisObject)
self.mpris_frontend.mpris_object = self.mpris_object
def test_track_playback_paused_event_changes_playback_status(self):
self.mpris_object.Get.return_value = 'Paused'
self.mpris_frontend.track_playback_paused(TlTrack(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, [])
def test_track_playback_resumed_event_changes_playback_status(self):
self.mpris_object.Get.return_value = 'Playing'
self.mpris_frontend.track_playback_resumed(TlTrack(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, [])
def test_track_playback_started_changes_playback_status_and_metadata(self):
self.mpris_object.Get.return_value = '...'
self.mpris_frontend.track_playback_started(TlTrack())
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
((objects.PLAYER_IFACE, 'Metadata'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE,
{'Metadata': '...', 'PlaybackStatus': '...'}, [])
def test_track_playback_ended_changes_playback_status_and_metadata(self):
self.mpris_object.Get.return_value = '...'
self.mpris_frontend.track_playback_ended(TlTrack(), 0)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'PlaybackStatus'), {}),
((objects.PLAYER_IFACE, 'Metadata'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE,
{'Metadata': '...', 'PlaybackStatus': '...'}, [])
def test_volume_changed_event_changes_volume(self):
self.mpris_object.Get.return_value = 1.0
self.mpris_frontend.volume_changed(volume=100)
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYER_IFACE, 'Volume'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYER_IFACE, {'Volume': 1.0}, [])
def test_seeked_event_causes_mpris_seeked_event(self):
self.mpris_frontend.seeked(31000)
self.mpris_object.Seeked.assert_called_with(31000000)
def test_playlists_loaded_event_changes_playlist_count(self):
self.mpris_object.Get.return_value = 17
self.mpris_frontend.playlists_loaded()
self.assertListEqual(self.mpris_object.Get.call_args_list, [
((objects.PLAYLISTS_IFACE, 'PlaylistCount'), {}),
])
self.mpris_object.PropertiesChanged.assert_called_with(
objects.PLAYLISTS_IFACE, {'PlaylistCount': 17}, [])
def test_playlist_changed_event_causes_mpris_playlist_changed_event(self):
self.mpris_object.get_playlist_id.return_value = 'id-for-dummy:foo'
playlist = Playlist(uri='dummy:foo', name='foo')
self.mpris_frontend.playlist_changed(playlist)
self.mpris_object.PlaylistChanged.assert_called_with(
('id-for-dummy:foo', 'foo', ''))

View File

@ -1,869 +0,0 @@
from __future__ import unicode_literals
import mock
import unittest
import pykka
try:
import dbus
except ImportError:
dbus = False
from mopidy import core
from mopidy.backends import dummy
from mopidy.core import PlaybackState
from mopidy.models import Album, Artist, Track
if dbus:
from mopidy.frontends.mpris import objects
PLAYING = PlaybackState.PLAYING
PAUSED = PlaybackState.PAUSED
STOPPED = PlaybackState.STOPPED
@unittest.skipUnless(dbus, 'dbus not found')
class PlayerInterfaceTest(unittest.TestCase):
def setUp(self):
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
self.mpris = objects.MprisObject(config={}, core=self.core)
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_get_playback_status_is_playing_when_playing(self):
self.core.playback.state = PLAYING
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Playing', result)
def test_get_playback_status_is_paused_when_paused(self):
self.core.playback.state = PAUSED
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Paused', result)
def test_get_playback_status_is_stopped_when_stopped(self):
self.core.playback.state = STOPPED
result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus')
self.assertEqual('Stopped', result)
def test_get_loop_status_is_none_when_not_looping(self):
self.core.playback.repeat = False
self.core.playback.single = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('None', result)
def test_get_loop_status_is_track_when_looping_a_single_track(self):
self.core.playback.repeat = True
self.core.playback.single = True
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('Track', result)
def test_get_loop_status_is_playlist_when_looping_tracklist(self):
self.core.playback.repeat = True
self.core.playback.single = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus')
self.assertEqual('Playlist', result)
def test_set_loop_status_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.playback.repeat = True
self.core.playback.single = True
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
self.assertEqual(self.core.playback.repeat.get(), True)
self.assertEqual(self.core.playback.single.get(), True)
def test_set_loop_status_to_none_unsets_repeat_and_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None')
self.assertEqual(self.core.playback.repeat.get(), False)
self.assertEqual(self.core.playback.single.get(), False)
def test_set_loop_status_to_track_sets_repeat_and_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Track')
self.assertEqual(self.core.playback.repeat.get(), True)
self.assertEqual(self.core.playback.single.get(), True)
def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self):
self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Playlist')
self.assertEqual(self.core.playback.repeat.get(), True)
self.assertEqual(self.core.playback.single.get(), False)
def test_get_rate_is_greater_or_equal_than_minimum_rate(self):
rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
minimum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
self.assertGreaterEqual(rate, minimum_rate)
def test_get_rate_is_less_or_equal_than_maximum_rate(self):
rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate')
maximum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
self.assertGreaterEqual(rate, maximum_rate)
def test_set_rate_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_set_rate_to_zero_pauses_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0)
self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_get_shuffle_returns_true_if_random_is_active(self):
self.core.playback.random = True
result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
self.assertTrue(result)
def test_get_shuffle_returns_false_if_random_is_inactive(self):
self.core.playback.random = False
result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle')
self.assertFalse(result)
def test_set_shuffle_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.playback.random = False
self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
self.assertFalse(self.core.playback.random.get())
def test_set_shuffle_to_true_activates_random_mode(self):
self.core.playback.random = False
self.assertFalse(self.core.playback.random.get())
self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True)
self.assertTrue(self.core.playback.random.get())
def test_set_shuffle_to_false_deactivates_random_mode(self):
self.core.playback.random = True
self.assertTrue(self.core.playback.random.get())
self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False)
self.assertFalse(self.core.playback.random.get())
def test_get_metadata_has_trackid_even_when_no_current_track(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:trackid', result.keys())
self.assertEqual(result['mpris:trackid'], '')
def test_get_metadata_has_trackid_based_on_tlid(self):
self.core.tracklist.add([Track(uri='dummy:a')])
self.core.playback.play()
(tlid, track) = self.core.playback.current_tl_track.get()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:trackid', result.keys())
self.assertEqual(
result['mpris:trackid'], '/com/mopidy/track/%d' % tlid)
def test_get_metadata_has_track_length(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:length', result.keys())
self.assertEqual(result['mpris:length'], 40000000)
def test_get_metadata_has_track_uri(self):
self.core.tracklist.add([Track(uri='dummy:a')])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:url', result.keys())
self.assertEqual(result['xesam:url'], 'dummy:a')
def test_get_metadata_has_track_title(self):
self.core.tracklist.add([Track(name='a')])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:title', result.keys())
self.assertEqual(result['xesam:title'], 'a')
def test_get_metadata_has_track_artists(self):
self.core.tracklist.add([Track(artists=[
Artist(name='a'), Artist(name='b'), Artist(name=None)])])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:artist', result.keys())
self.assertEqual(result['xesam:artist'], ['a', 'b'])
def test_get_metadata_has_track_album(self):
self.core.tracklist.add([Track(album=Album(name='a'))])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:album', result.keys())
self.assertEqual(result['xesam:album'], 'a')
def test_get_metadata_has_track_album_artists(self):
self.core.tracklist.add([Track(album=Album(artists=[
Artist(name='a'), Artist(name='b'), Artist(name=None)]))])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:albumArtist', result.keys())
self.assertEqual(result['xesam:albumArtist'], ['a', 'b'])
def test_get_metadata_use_first_album_image_as_art_url(self):
# XXX Currently, the album image order isn't preserved because they
# are stored as a frozenset(). We pick the first in the set, which is
# sorted alphabetically, thus we get 'bar.jpg', not 'foo.jpg', which
# would probably make more sense.
self.core.tracklist.add([Track(album=Album(images=[
'http://example.com/foo.jpg', 'http://example.com/bar.jpg']))])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('mpris:artUrl', result.keys())
self.assertEqual(result['mpris:artUrl'], 'http://example.com/bar.jpg')
def test_get_metadata_has_no_art_url_if_no_album(self):
self.core.tracklist.add([Track()])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertNotIn('mpris:artUrl', result.keys())
def test_get_metadata_has_no_art_url_if_no_album_images(self):
self.core.tracklist.add([Track(Album(images=[]))])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertNotIn('mpris:artUrl', result.keys())
def test_get_metadata_has_disc_number_in_album(self):
self.core.tracklist.add([Track(disc_no=2)])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:discNumber', result.keys())
self.assertEqual(result['xesam:discNumber'], 2)
def test_get_metadata_has_track_number_in_album(self):
self.core.tracklist.add([Track(track_no=7)])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata')
self.assertIn('xesam:trackNumber', result.keys())
self.assertEqual(result['xesam:trackNumber'], 7)
def test_get_volume_should_return_volume_between_zero_and_one(self):
self.core.playback.volume = None
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEqual(result, 0)
self.core.playback.volume = 0
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEqual(result, 0)
self.core.playback.volume = 50
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEqual(result, 0.5)
self.core.playback.volume = 100
result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume')
self.assertEqual(result, 1)
def test_set_volume_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.playback.volume = 0
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
self.assertEqual(self.core.playback.volume.get(), 0)
def test_set_volume_to_one_should_set_mixer_volume_to_100(self):
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0)
self.assertEqual(self.core.playback.volume.get(), 100)
def test_set_volume_to_anything_above_one_sets_mixer_volume_to_100(self):
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0)
self.assertEqual(self.core.playback.volume.get(), 100)
def test_set_volume_to_anything_not_a_number_does_not_change_volume(self):
self.core.playback.volume = 10
self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None)
self.assertEqual(self.core.playback.volume.get(), 10)
def test_get_position_returns_time_position_in_microseconds(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.core.playback.seek(10000)
result_in_microseconds = self.mpris.Get(
objects.PLAYER_IFACE, 'Position')
result_in_milliseconds = result_in_microseconds // 1000
self.assertGreaterEqual(result_in_milliseconds, 10000)
def test_get_position_when_no_current_track_should_be_zero(self):
result_in_microseconds = self.mpris.Get(
objects.PLAYER_IFACE, 'Position')
result_in_milliseconds = result_in_microseconds // 1000
self.assertEqual(result_in_milliseconds, 0)
def test_get_minimum_rate_is_one_or_less(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate')
self.assertLessEqual(result, 1.0)
def test_get_maximum_rate_is_one_or_more(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate')
self.assertGreaterEqual(result, 1.0)
def test_can_go_next_is_true_if_can_control_and_other_next_track(self):
self.mpris.get_CanControl = lambda *_: True
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertTrue(result)
def test_can_go_next_is_false_if_next_track_is_the_same(self):
self.mpris.get_CanControl = lambda *_: True
self.core.tracklist.add([Track(uri='dummy:a')])
self.core.playback.repeat = True
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertFalse(result)
def test_can_go_next_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext')
self.assertFalse(result)
def test_can_go_previous_is_true_if_can_control_and_previous_track(self):
self.mpris.get_CanControl = lambda *_: True
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertTrue(result)
def test_can_go_previous_is_false_if_previous_track_is_the_same(self):
self.mpris.get_CanControl = lambda *_: True
self.core.tracklist.add([Track(uri='dummy:a')])
self.core.playback.repeat = True
self.core.playback.play()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertFalse(result)
def test_can_go_previous_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious')
self.assertFalse(result)
def test_can_play_is_true_if_can_control_and_current_track(self):
self.mpris.get_CanControl = lambda *_: True
self.core.tracklist.add([Track(uri='dummy:a')])
self.core.playback.play()
self.assertTrue(self.core.playback.current_track.get())
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertTrue(result)
def test_can_play_is_false_if_no_current_track(self):
self.mpris.get_CanControl = lambda *_: True
self.assertFalse(self.core.playback.current_track.get())
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertFalse(result)
def test_can_play_if_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay')
self.assertFalse(result)
def test_can_pause_is_true_if_can_control_and_track_can_be_paused(self):
self.mpris.get_CanControl = lambda *_: True
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause')
self.assertTrue(result)
def test_can_pause_if_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause')
self.assertFalse(result)
def test_can_seek_is_true_if_can_control_is_true(self):
self.mpris.get_CanControl = lambda *_: True
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek')
self.assertTrue(result)
def test_can_seek_is_false_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek')
self.assertFalse(result)
def test_can_control_is_true(self):
result = self.mpris.Get(objects.PLAYER_IFACE, 'CanControl')
self.assertTrue(result)
def test_next_is_ignored_if_can_go_next_is_false(self):
self.mpris.get_CanGoNext = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.mpris.Next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
def test_next_when_playing_skips_to_next_track_and_keep_playing(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_next_when_at_end_of_list_should_stop_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Next()
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.pause()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_next_when_stopped_skips_to_next_track_and_stay_stopped(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.stop()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_previous_is_ignored_if_can_go_previous_is_false(self):
self.mpris.get_CanGoPrevious = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.mpris.Previous()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
def test_previous_when_playing_skips_to_prev_track_and_keep_playing(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Previous()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_previous_when_at_start_of_list_should_stop_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Previous()
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_previous_when_paused_skips_to_previous_track_and_pause(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
self.core.playback.pause()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Previous()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_previous_when_stopped_skips_to_previous_track_and_stops(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.next()
self.core.playback.stop()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Previous()
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_pause_is_ignored_if_can_pause_is_false(self):
self.mpris.get_CanPause = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Pause()
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_pause_when_playing_should_pause_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_pause_when_paused_has_no_effect(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_playpause_is_ignored_if_can_pause_is_false(self):
self.mpris.get_CanPause = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.PlayPause()
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_playpause_when_playing_should_pause_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.PlayPause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
def test_playpause_when_paused_should_resume_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
at_pause = self.core.playback.time_position.get()
self.assertGreaterEqual(at_pause, 0)
self.mpris.PlayPause()
self.assertEqual(self.core.playback.state.get(), PLAYING)
after_pause = self.core.playback.time_position.get()
self.assertGreaterEqual(after_pause, at_pause)
def test_playpause_when_stopped_should_start_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.PlayPause()
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_stop_is_ignored_if_can_control_is_false(self):
self.mpris.get_CanControl = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Stop()
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_stop_when_playing_should_stop_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.mpris.Stop()
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_stop_when_paused_should_stop_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
self.mpris.Stop()
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_play_is_ignored_if_can_play_is_false(self):
self.mpris.get_CanPlay = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Play()
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_play_when_stopped_starts_playback(self):
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
def test_play_after_pause_resumes_from_same_position(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
before_pause = self.core.playback.time_position.get()
self.assertGreaterEqual(before_pause, 0)
self.mpris.Pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
at_pause = self.core.playback.time_position.get()
self.assertGreaterEqual(at_pause, before_pause)
self.mpris.Play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
after_pause = self.core.playback.time_position.get()
self.assertGreaterEqual(after_pause, at_pause)
def test_play_when_there_is_no_track_has_no_effect(self):
self.core.tracklist.clear()
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.Play()
self.assertEqual(self.core.playback.state.get(), STOPPED)
def test_seek_is_ignored_if_can_seek_is_false(self):
self.mpris.get_CanSeek = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
before_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(before_seek, 0)
milliseconds_to_seek = 10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
after_seek = self.core.playback.time_position.get()
self.assertLessEqual(before_seek, after_seek)
self.assertLess(after_seek, before_seek + milliseconds_to_seek)
def test_seek_seeks_given_microseconds_forward_in_the_current_track(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
before_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(before_seek, 0)
milliseconds_to_seek = 10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEqual(self.core.playback.state.get(), PLAYING)
after_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek)
def test_seek_seeks_given_microseconds_backward_if_negative(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.core.playback.seek(20000)
before_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(before_seek, 20000)
milliseconds_to_seek = -10000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEqual(self.core.playback.state.get(), PLAYING)
after_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek)
self.assertLess(after_seek, before_seek)
def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.core.playback.seek(20000)
before_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(before_seek, 20000)
milliseconds_to_seek = -30000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEqual(self.core.playback.state.get(), PLAYING)
after_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek)
self.assertLess(after_seek, before_seek)
self.assertGreaterEqual(after_seek, 0)
def test_seek_skips_to_next_track_if_new_position_gt_track_length(self):
self.core.tracklist.add([
Track(uri='dummy:a', length=40000),
Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.seek(20000)
before_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(before_seek, 20000)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
milliseconds_to_seek = 50000
microseconds_to_seek = milliseconds_to_seek * 1000
self.mpris.Seek(microseconds_to_seek)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b')
after_seek = self.core.playback.time_position.get()
self.assertGreaterEqual(after_seek, 0)
self.assertLess(after_seek, before_seek)
def test_set_position_is_ignored_if_can_seek_is_false(self):
self.mpris.get_CanSeek = lambda *_: False
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
before_set_position = self.core.playback.time_position.get()
self.assertLessEqual(before_set_position, 5000)
track_id = 'a'
position_to_set_in_millisec = 20000
position_to_set_in_microsec = position_to_set_in_millisec * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microsec)
after_set_position = self.core.playback.time_position.get()
self.assertLessEqual(before_set_position, after_set_position)
self.assertLess(after_set_position, position_to_set_in_millisec)
def test_set_position_sets_the_current_track_position_in_microsecs(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
before_set_position = self.core.playback.time_position.get()
self.assertLessEqual(before_set_position, 5000)
self.assertEqual(self.core.playback.state.get(), PLAYING)
track_id = '/com/mopidy/track/0'
position_to_set_in_millisec = 20000
position_to_set_in_microsec = position_to_set_in_millisec * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microsec)
self.assertEqual(self.core.playback.state.get(), PLAYING)
after_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(
after_set_position, position_to_set_in_millisec)
def test_set_position_does_nothing_if_the_position_is_negative(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.core.playback.seek(20000)
before_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(before_set_position, 20000)
self.assertLessEqual(before_set_position, 25000)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
track_id = '/com/mopidy/track/0'
position_to_set_in_millisec = -1000
position_to_set_in_microsec = position_to_set_in_millisec * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microsec)
after_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(after_set_position, before_set_position)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
def test_set_position_does_nothing_if_position_is_gt_track_length(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.core.playback.seek(20000)
before_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(before_set_position, 20000)
self.assertLessEqual(before_set_position, 25000)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
track_id = 'a'
position_to_set_in_millisec = 50000
position_to_set_in_microsec = position_to_set_in_millisec * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microsec)
after_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(after_set_position, before_set_position)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
def test_set_position_is_noop_if_track_id_isnt_current_track(self):
self.core.tracklist.add([Track(uri='dummy:a', length=40000)])
self.core.playback.play()
self.core.playback.seek(20000)
before_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(before_set_position, 20000)
self.assertLessEqual(before_set_position, 25000)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
track_id = 'b'
position_to_set_in_millisec = 0
position_to_set_in_microsec = position_to_set_in_millisec * 1000
self.mpris.SetPosition(track_id, position_to_set_in_microsec)
after_set_position = self.core.playback.time_position.get()
self.assertGreaterEqual(after_set_position, before_set_position)
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
def test_open_uri_is_ignored_if_can_play_is_false(self):
self.mpris.get_CanPlay = lambda *_: False
self.backend.library.dummy_library = [
Track(uri='dummy:/test/uri')]
self.mpris.OpenUri('dummy:/test/uri')
self.assertEqual(len(self.core.tracklist.tracks.get()), 0)
def test_open_uri_ignores_uris_with_unknown_uri_scheme(self):
self.assertListEqual(self.core.uri_schemes.get(), ['dummy'])
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.dummy_library = [Track(uri='notdummy:/test/uri')]
self.mpris.OpenUri('notdummy:/test/uri')
self.assertEqual(len(self.core.tracklist.tracks.get()), 0)
def test_open_uri_adds_uri_to_tracklist(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
self.mpris.OpenUri('dummy:/test/uri')
self.assertEqual(
self.core.tracklist.tracks.get()[0].uri, 'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_stopped(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.assertEqual(self.core.playback.state.get(), STOPPED)
self.mpris.OpenUri('dummy:/test/uri')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(
self.core.playback.current_track.get().uri, 'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_paused(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.core.playback.pause()
self.assertEqual(self.core.playback.state.get(), PAUSED)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.mpris.OpenUri('dummy:/test/uri')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(
self.core.playback.current_track.get().uri, 'dummy:/test/uri')
def test_open_uri_starts_playback_of_new_track_if_playing(self):
self.mpris.get_CanPlay = lambda *_: True
self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')]
self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')])
self.core.playback.play()
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a')
self.mpris.OpenUri('dummy:/test/uri')
self.assertEqual(self.core.playback.state.get(), PLAYING)
self.assertEqual(
self.core.playback.current_track.get().uri, 'dummy:/test/uri')

View File

@ -1,172 +0,0 @@
from __future__ import unicode_literals
import datetime
import mock
import unittest
import pykka
try:
import dbus
except ImportError:
dbus = False
from mopidy import core
from mopidy.audio import PlaybackState
from mopidy.backends import dummy
from mopidy.models import Track
if dbus:
from mopidy.frontends.mpris import objects
@unittest.skipUnless(dbus, 'dbus not found')
class PlayerInterfaceTest(unittest.TestCase):
def setUp(self):
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
self.mpris = objects.MprisObject(config={}, core=self.core)
foo = self.core.playlists.create('foo').get()
foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0))
foo = self.core.playlists.save(foo).get()
bar = self.core.playlists.create('bar').get()
bar = bar.copy(last_modified=datetime.datetime(2012, 2, 1, 6, 0, 0))
bar = self.core.playlists.save(bar).get()
baz = self.core.playlists.create('baz').get()
baz = baz.copy(last_modified=datetime.datetime(2012, 1, 1, 6, 0, 0))
baz = self.core.playlists.save(baz).get()
self.playlist = baz
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_activate_playlist_appends_tracks_to_tracklist(self):
self.core.tracklist.add([
Track(uri='dummy:old-a'),
Track(uri='dummy:old-b'),
])
self.playlist = self.playlist.copy(tracks=[
Track(uri='dummy:baz-a'),
Track(uri='dummy:baz-b'),
Track(uri='dummy:baz-c'),
])
self.playlist = self.core.playlists.save(self.playlist).get()
self.assertEqual(2, self.core.tracklist.length.get())
playlists = self.mpris.GetPlaylists(0, 100, 'User', False)
playlist_id = playlists[2][0]
self.mpris.ActivatePlaylist(playlist_id)
self.assertEqual(5, self.core.tracklist.length.get())
self.assertEqual(
PlaybackState.PLAYING, self.core.playback.state.get())
self.assertEqual(
self.playlist.tracks[0], self.core.playback.current_track.get())
def test_activate_empty_playlist_is_harmless(self):
self.assertEqual(0, self.core.tracklist.length.get())
playlists = self.mpris.GetPlaylists(0, 100, 'User', False)
playlist_id = playlists[2][0]
self.mpris.ActivatePlaylist(playlist_id)
self.assertEqual(0, self.core.tracklist.length.get())
self.assertEqual(
PlaybackState.STOPPED, self.core.playback.state.get())
self.assertIsNone(self.core.playback.current_track.get())
def test_get_playlists_in_alphabetical_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', False)
self.assertEqual(3, len(result))
self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC4Q_', result[0][0])
self.assertEqual('bar', result[0][1])
self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC6Q_', result[1][0])
self.assertEqual('baz', result[1][1])
self.assertEqual('/com/mopidy/playlist/MR2W23LZHJTG63Y_', result[2][0])
self.assertEqual('foo', result[2][1])
def test_get_playlists_in_reverse_alphabetical_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', True)
self.assertEqual(3, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('baz', result[1][1])
self.assertEqual('bar', result[2][1])
def test_get_playlists_in_modified_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Modified', False)
self.assertEqual(3, len(result))
self.assertEqual('baz', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('foo', result[2][1])
def test_get_playlists_in_reverse_modified_order(self):
result = self.mpris.GetPlaylists(0, 100, 'Modified', True)
self.assertEqual(3, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('baz', result[2][1])
def test_get_playlists_in_user_order(self):
result = self.mpris.GetPlaylists(0, 100, 'User', False)
self.assertEqual(3, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('baz', result[2][1])
def test_get_playlists_in_reverse_user_order(self):
result = self.mpris.GetPlaylists(0, 100, 'User', True)
self.assertEqual(3, len(result))
self.assertEqual('baz', result[0][1])
self.assertEqual('bar', result[1][1])
self.assertEqual('foo', result[2][1])
def test_get_playlists_slice_on_start_of_list(self):
result = self.mpris.GetPlaylists(0, 2, 'User', False)
self.assertEqual(2, len(result))
self.assertEqual('foo', result[0][1])
self.assertEqual('bar', result[1][1])
def test_get_playlists_slice_later_in_list(self):
result = self.mpris.GetPlaylists(2, 2, 'User', False)
self.assertEqual(1, len(result))
self.assertEqual('baz', result[0][1])
def test_get_playlist_count_returns_number_of_playlists(self):
result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'PlaylistCount')
self.assertEqual(3, result)
def test_get_orderings_includes_alpha_modified_and_user(self):
result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'Orderings')
self.assertIn('Alphabetical', result)
self.assertNotIn('Created', result)
self.assertIn('Modified', result)
self.assertNotIn('Played', result)
self.assertIn('User', result)
def test_get_active_playlist_does_not_return_a_playlist(self):
result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'ActivePlaylist')
valid, playlist = result
playlist_id, playlist_name, playlist_icon_uri = playlist
self.assertEqual(False, valid)
self.assertEqual('/', playlist_id)
self.assertEqual('None', playlist_name)
self.assertEqual('', playlist_icon_uri)

View File

@ -1,87 +0,0 @@
from __future__ import unicode_literals
import mock
import unittest
import pykka
try:
import dbus
except ImportError:
dbus = False
from mopidy import core
from mopidy.backends import dummy
if dbus:
from mopidy.frontends.mpris import objects
@unittest.skipUnless(dbus, 'dbus not found')
class RootInterfaceTest(unittest.TestCase):
def setUp(self):
config = {
'mpris': {
'desktop_file': '/tmp/foo.desktop',
}
}
objects.exit_process = mock.Mock()
objects.MprisObject._connect_to_dbus = mock.Mock()
self.backend = dummy.create_dummy_backend_proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
self.mpris = objects.MprisObject(config=config, core=self.core)
def tearDown(self):
pykka.ActorRegistry.stop_all()
def test_constructor_connects_to_dbus(self):
self.assert_(self.mpris._connect_to_dbus.called)
def test_fullscreen_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'Fullscreen')
self.assertFalse(result)
def test_setting_fullscreen_fails_and_returns_none(self):
result = self.mpris.Set(objects.ROOT_IFACE, 'Fullscreen', 'True')
self.assertIsNone(result)
def test_can_set_fullscreen_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanSetFullscreen')
self.assertFalse(result)
def test_can_raise_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise')
self.assertFalse(result)
def test_raise_does_nothing(self):
self.mpris.Raise()
def test_can_quit_returns_true(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'CanQuit')
self.assertTrue(result)
def test_quit_should_stop_all_actors(self):
self.mpris.Quit()
self.assert_(objects.exit_process.called)
def test_has_track_list_returns_false(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'HasTrackList')
self.assertFalse(result)
def test_identify_is_mopidy(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'Identity')
self.assertEquals(result, 'Mopidy')
def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry')
self.assertEquals(result, 'foo')
def test_supported_uri_schemes_includes_backend_uri_schemes(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes')
self.assertEquals(len(result), 1)
self.assertEquals(result[0], 'dummy')
def test_supported_mime_types_is_empty(self):
result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedMimeTypes')
self.assertEquals(len(result), 0)

View File

@ -2,8 +2,9 @@ from __future__ import unicode_literals
import unittest
from mopidy.scanner import Scanner, translator
from mopidy import exceptions
from mopidy.models import Track, Artist, Album
from mopidy.scanner import Scanner, translator
from mopidy.utils import path as path_lib
from tests import path_to_data_dir
@ -150,21 +151,18 @@ class ScannerTest(unittest.TestCase):
def scan(self, path):
paths = path_lib.find_files(path_to_data_dir(path))
uris = (path_lib.path_to_uri(p) for p in paths)
scanner = Scanner(uris, self.data_callback, self.error_callback)
scanner.start()
scanner = Scanner()
for uri in uris:
key = uri[len('file://'):]
try:
self.data[key] = scanner.scan(uri)
except exceptions.ScannerError as error:
self.errors[key] = error
def check(self, name, key, value):
name = path_to_data_dir(name)
self.assertEqual(self.data[name][key], value)
def data_callback(self, data):
uri = data['uri'][len('file://'):]
self.data[uri] = data
def error_callback(self, uri, error, debug):
uri = uri[len('file://'):]
self.errors[uri] = (error, debug)
def test_data_is_set(self):
self.scan('scanner/simple')
self.assert_(self.data)
@ -210,7 +208,7 @@ class ScannerTest(unittest.TestCase):
self.scan('scanner/image')
self.assert_(self.errors)
def test_log_file_is_ignored(self):
def test_log_file_that_gst_thinks_is_mpeg_1_is_ignored(self):
self.scan('scanner/example.log')
self.assert_(self.errors)

View File

@ -34,6 +34,9 @@ class Calculator(object):
def _secret(self):
return 'Grand Unified Theory'
def fail(self):
raise ValueError('What did you expect?')
class JsonRpcTestBase(unittest.TestCase):
def setUp(self):
@ -266,12 +269,12 @@ class JsonRpcSingleNotificationTest(JsonRpcTestBase):
class JsonRpcBatchTest(JsonRpcTestBase):
def test_batch_of_only_commands_returns_all(self):
self.core.playback.set_random(True).get()
self.core.tracklist.set_random(True).get()
request = [
{'jsonrpc': '2.0', 'method': 'core.playback.get_repeat', 'id': 1},
{'jsonrpc': '2.0', 'method': 'core.playback.get_random', 'id': 2},
{'jsonrpc': '2.0', 'method': 'core.playback.get_single', 'id': 3},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat', 'id': 1},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': 2},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_single', 'id': 3},
]
response = self.jrw.handle_data(request)
@ -283,12 +286,12 @@ class JsonRpcBatchTest(JsonRpcTestBase):
self.assertEqual(response[3]['result'], False)
def test_batch_of_commands_and_notifications_returns_some(self):
self.core.playback.set_random(True).get()
self.core.tracklist.set_random(True).get()
request = [
{'jsonrpc': '2.0', 'method': 'core.playback.get_repeat'},
{'jsonrpc': '2.0', 'method': 'core.playback.get_random', 'id': 2},
{'jsonrpc': '2.0', 'method': 'core.playback.get_single', 'id': 3},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat'},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': 2},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_single', 'id': 3},
]
response = self.jrw.handle_data(request)
@ -300,12 +303,12 @@ class JsonRpcBatchTest(JsonRpcTestBase):
self.assertEqual(response[3]['result'], False)
def test_batch_of_only_notifications_returns_nothing(self):
self.core.playback.set_random(True).get()
self.core.tracklist.set_random(True).get()
request = [
{'jsonrpc': '2.0', 'method': 'core.playback.get_repeat'},
{'jsonrpc': '2.0', 'method': 'core.playback.get_random'},
{'jsonrpc': '2.0', 'method': 'core.playback.get_single'},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat'},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_random'},
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_single'},
]
response = self.jrw.handle_data(request)
@ -316,8 +319,8 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
def test_application_error_response(self):
request = {
'jsonrpc': '2.0',
'method': 'core.tracklist.index',
'params': ['bogus'],
'method': 'calc.fail',
'params': [],
'id': 1,
}
response = self.jrw.handle_data(request)
@ -330,7 +333,7 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
data = error['data']
self.assertEqual(data['type'], 'ValueError')
self.assertIn('not in list', data['message'])
self.assertIn('What did you expect?', data['message'])
self.assertIn('traceback', data)
self.assertIn('Traceback (most recent call last):', data['traceback'])
@ -522,10 +525,10 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase):
{'jsonrpc': '2.0', 'method': 'core.playback.set_volume',
'params': [47], 'id': '1'},
# Notification
{'jsonrpc': '2.0', 'method': 'core.playback.set_consume',
{'jsonrpc': '2.0', 'method': 'core.tracklist.set_consume',
'params': [True]},
# Call with positional params
{'jsonrpc': '2.0', 'method': 'core.playback.set_repeat',
{'jsonrpc': '2.0', 'method': 'core.tracklist.set_repeat',
'params': [False], 'id': '2'},
# Invalid request
{'foo': 'boo'},
@ -533,7 +536,7 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase):
{'jsonrpc': '2.0', 'method': 'foo.get',
'params': {'name': 'myself'}, 'id': '5'},
# Call without params
{'jsonrpc': '2.0', 'method': 'core.playback.get_random',
{'jsonrpc': '2.0', 'method': 'core.tracklist.get_random',
'id': '9'},
]
response = self.jrw.handle_data(request)

View File

@ -39,5 +39,6 @@ class VersionTest(unittest.TestCase):
self.assertLess(SV('0.13.0'), SV('0.14.0'))
self.assertLess(SV('0.14.0'), SV('0.14.1'))
self.assertLess(SV('0.14.1'), SV('0.14.2'))
self.assertLess(SV('0.14.2'), SV(__version__))
self.assertLess(SV(__version__), SV('0.15.1'))
self.assertLess(SV('0.14.2'), SV('0.15.0'))
self.assertLess(SV('0.15.0'), SV(__version__))
self.assertLess(SV(__version__), SV('0.16.1'))